Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
05ff87d0b8
|
20
README.md
20
README.md
@@ -9,6 +9,9 @@ A Baseboard Management Controller (BMC) firmware for ESP32-S3 that provides remo
|
|||||||
- **Serial Console**: UART bridge with WebSocket support for remote console access
|
- **Serial Console**: UART bridge with WebSocket support for remote console access
|
||||||
- **Web Interface**: Modern HTML/CSS dashboard for easy management
|
- **Web Interface**: Modern HTML/CSS dashboard for easy management
|
||||||
- **REST API**: Full REST API for programmatic control
|
- **REST API**: Full REST API for programmatic control
|
||||||
|
- **HTTP Basic Authentication**: Secure access to web interface and API
|
||||||
|
- **OTA Updates**: Over-the-air firmware updates
|
||||||
|
- **System Logs**: View ESP32 logs via web interface or API
|
||||||
|
|
||||||
## Hardware Requirements
|
## Hardware Requirements
|
||||||
|
|
||||||
@@ -78,6 +81,23 @@ idf.py -p /dev/ttyUSB0 monitor
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
The web interface and API are protected by HTTP Basic Authentication. Default credentials:
|
||||||
|
- **Username**: `admin`
|
||||||
|
- **Password**: `admin`
|
||||||
|
|
||||||
|
To change credentials, edit `main/config.h`:
|
||||||
|
```c
|
||||||
|
#define BMC_HTTP_AUTH_USERNAME "your_username"
|
||||||
|
#define BMC_HTTP_AUTH_PASSWORD "your_password"
|
||||||
|
```
|
||||||
|
|
||||||
|
To disable authentication:
|
||||||
|
```c
|
||||||
|
#define BMC_HTTP_AUTH_ENABLED 0
|
||||||
|
```
|
||||||
|
|
||||||
### Network Settings
|
### Network Settings
|
||||||
|
|
||||||
By default, the firmware uses DHCP. To configure static IP:
|
By default, the firmware uses DHCP. To configure static IP:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ idf_component_register(SRCS "main.c"
|
|||||||
INCLUDE_DIRS "."
|
INCLUDE_DIRS "."
|
||||||
REQUIRES esp_driver_gpio esp_driver_uart esp_driver_spi
|
REQUIRES esp_driver_gpio esp_driver_uart esp_driver_spi
|
||||||
esp_http_server esp_eth esp_netif lwip spi_flash nvs_flash log
|
esp_http_server esp_eth esp_netif lwip spi_flash nvs_flash log
|
||||||
esp_https_ota esp_http_client app_update)
|
esp_https_ota esp_http_client app_update mbedtls)
|
||||||
|
|
||||||
# Embed HTML file
|
# Embed HTML file
|
||||||
target_add_binary_data(${COMPONENT_TARGET} "html/index.html" TEXT)
|
target_add_binary_data(${COMPONENT_TARGET} "html/index.html" TEXT)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
// UART Serial Configuration
|
// UART Serial Configuration
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
#define BMC_UART_NUM UART_NUM_1
|
#define BMC_UART_NUM UART_NUM_0
|
||||||
#define BMC_UART_TX_GPIO GPIO_NUM_43
|
#define BMC_UART_TX_GPIO GPIO_NUM_43
|
||||||
#define BMC_UART_RX_GPIO GPIO_NUM_44
|
#define BMC_UART_RX_GPIO GPIO_NUM_44
|
||||||
#define BMC_UART_BAUD_RATE 115200
|
#define BMC_UART_BAUD_RATE 115200
|
||||||
@@ -42,6 +42,12 @@
|
|||||||
#define BMC_HTTP_MAX_CONN 17
|
#define BMC_HTTP_MAX_CONN 17
|
||||||
#define BMC_HTTP_STACK_SIZE 8192
|
#define BMC_HTTP_STACK_SIZE 8192
|
||||||
|
|
||||||
|
// HTTP Basic Authentication
|
||||||
|
#define BMC_HTTP_AUTH_ENABLED 1
|
||||||
|
#define BMC_HTTP_AUTH_USERNAME "admin"
|
||||||
|
#define BMC_HTTP_AUTH_PASSWORD "admin"
|
||||||
|
#define BMC_HTTP_AUTH_REALM "ESP32 BMC"
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// GPIO Configuration
|
// GPIO Configuration
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -524,10 +524,109 @@
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Login Modal Styles */
|
||||||
|
.login-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-overlay.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-modal {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 40px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-modal h2 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 1.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-modal p {
|
||||||
|
color: #aaa;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-field {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-field label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-field input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1em;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-field input:focus {
|
||||||
|
border-color: #00d4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-error {
|
||||||
|
color: #ff4444;
|
||||||
|
margin-top: 15px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<!-- Login Modal -->
|
||||||
|
<div class="login-overlay" id="login-overlay">
|
||||||
|
<div class="login-modal">
|
||||||
|
<h2>🔐 BMC Login</h2>
|
||||||
|
<p>Enter your credentials to access the BMC dashboard</p>
|
||||||
|
<form id="login-form" onsubmit="return handleLogin(event)">
|
||||||
|
<div class="login-field">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input type="text" id="username" name="username" required autocomplete="username">
|
||||||
|
</div>
|
||||||
|
<div class="login-field">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" id="password" name="password" required autocomplete="current-password">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary login-btn">Login</button>
|
||||||
|
<div class="login-error" id="login-error"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container" id="main-container" style="display: none;">
|
||||||
<header>
|
<header>
|
||||||
<h1>🖥️ VisionFive2 BMC Dashboard</h1>
|
<h1>🖥️ VisionFive2 BMC Dashboard</h1>
|
||||||
<div class="status-bar">
|
<div class="status-bar">
|
||||||
@@ -717,6 +816,122 @@
|
|||||||
// API base URL
|
// API base URL
|
||||||
const API_BASE = '';
|
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
|
// Toast notification
|
||||||
function showToast(message, type = 'success') {
|
function showToast(message, type = 'success') {
|
||||||
const container = document.getElementById('toast-container');
|
const container = document.getElementById('toast-container');
|
||||||
@@ -730,8 +945,9 @@
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// API helper
|
// API helper with authentication
|
||||||
async function apiCall(endpoint, method = 'GET', data = null) {
|
async function apiCall(endpoint, method = 'GET', data = null) {
|
||||||
|
const authHeader = getAuthHeader();
|
||||||
const options = {
|
const options = {
|
||||||
method: method,
|
method: method,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -739,12 +955,25 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (authHeader) {
|
||||||
|
options.headers['Authorization'] = authHeader;
|
||||||
|
}
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
options.body = JSON.stringify(data);
|
options.body = JSON.stringify(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}${endpoint}`, options);
|
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();
|
const result = await response.json();
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
@@ -858,7 +1087,14 @@
|
|||||||
|
|
||||||
function connectWebSocket() {
|
function connectWebSocket() {
|
||||||
const protocol = window.location.protocol === 'https' ? 'wss' : 'ws';
|
const protocol = window.location.protocol === 'https' ? 'wss' : 'ws';
|
||||||
const wsUrl = `${protocol}://${window.location.host}/api/serial/ws`;
|
const auth = getStoredCredentials();
|
||||||
|
let wsUrl = `${protocol}://${window.location.host}/api/serial/ws`;
|
||||||
|
|
||||||
|
// Add auth parameter for WebSocket authentication
|
||||||
|
if (auth) {
|
||||||
|
const authB64 = btoa(auth.username + ':' + auth.password);
|
||||||
|
wsUrl += `?auth=${encodeURIComponent(authB64)}`;
|
||||||
|
}
|
||||||
|
|
||||||
ws = new WebSocket(wsUrl);
|
ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
@@ -1168,6 +1404,11 @@
|
|||||||
showToast('Firmware uploaded successfully! Device will restart...', 'success');
|
showToast('Firmware uploaded successfully! Device will restart...', 'success');
|
||||||
document.getElementById('ota-progress-bar').style.width = '100%';
|
document.getElementById('ota-progress-bar').style.width = '100%';
|
||||||
document.getElementById('ota-progress-text').textContent = '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 {
|
} else {
|
||||||
let errorMsg = 'Upload failed';
|
let errorMsg = 'Upload failed';
|
||||||
try {
|
try {
|
||||||
@@ -1187,6 +1428,13 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
xhr.open('POST', `${API_BASE}/api/ota/upload`);
|
xhr.open('POST', `${API_BASE}/api/ota/upload`);
|
||||||
|
|
||||||
|
// Add authentication header
|
||||||
|
const authHeader = getAuthHeader();
|
||||||
|
if (authHeader) {
|
||||||
|
xhr.setRequestHeader('Authorization', authHeader);
|
||||||
|
}
|
||||||
|
|
||||||
xhr.send(formData);
|
xhr.send(formData);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1197,8 +1445,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize
|
// Initialize function called after successful login
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
function initialize() {
|
||||||
updateSystemInfo();
|
updateSystemInfo();
|
||||||
updateGPIOStates();
|
updateGPIOStates();
|
||||||
updatePowerStatus();
|
updatePowerStatus();
|
||||||
@@ -1243,6 +1491,11 @@
|
|||||||
setInterval(updateGPIOStates, 2000);
|
setInterval(updateGPIOStates, 2000);
|
||||||
setInterval(updatePowerStatus, 2000);
|
setInterval(updatePowerStatus, 2000);
|
||||||
setInterval(updateOTAStatus, 5000);
|
setInterval(updateOTAStatus, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check authentication on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
checkAuth();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log viewer functions
|
// Log viewer functions
|
||||||
@@ -1250,7 +1503,18 @@
|
|||||||
|
|
||||||
async function refreshLogs() {
|
async function refreshLogs() {
|
||||||
try {
|
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 logs = await response.text();
|
||||||
const logsOutput = document.getElementById('logs-output');
|
const logsOutput = document.getElementById('logs-output');
|
||||||
logsOutput.innerHTML = `<pre>${escapeHtml(logs)}</pre>`;
|
logsOutput.innerHTML = `<pre>${escapeHtml(logs)}</pre>`;
|
||||||
@@ -1264,7 +1528,19 @@
|
|||||||
|
|
||||||
async function clearLogs() {
|
async function clearLogs() {
|
||||||
try {
|
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();
|
refreshLogs();
|
||||||
showToast('Logs cleared', 'success');
|
showToast('Logs cleared', 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "log_handler.h"
|
#include "log_handler.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "cJSON.h"
|
#include "cJSON.h"
|
||||||
|
#include "mbedtls/base64.h"
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <sys/param.h>
|
#include <sys/param.h>
|
||||||
|
|
||||||
@@ -66,6 +67,122 @@ static esp_err_t send_json_success(httpd_req_t *req, cJSON *data) {
|
|||||||
return ret;
|
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
|
// Static File Handlers
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -387,6 +504,15 @@ static void uart_to_ws_callback(const uint8_t *data, size_t len) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static esp_err_t ws_serial_handler(httpd_req_t *req) {
|
static esp_err_t ws_serial_handler(httpd_req_t *req) {
|
||||||
|
#if BMC_HTTP_AUTH_ENABLED
|
||||||
|
// Check authentication on WebSocket handshake
|
||||||
|
// The Authorization header is available during the initial HTTP upgrade request
|
||||||
|
if (!check_auth(req)) {
|
||||||
|
// Send 401 response - this will fail the WebSocket handshake
|
||||||
|
return send_auth_required(req);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// For WebSocket handlers with is_websocket=true, ESP-IDF handles the handshake
|
// For WebSocket handlers with is_websocket=true, ESP-IDF handles the handshake
|
||||||
// internally. This handler is only called for frame processing.
|
// internally. This handler is only called for frame processing.
|
||||||
// Client tracking is done dynamically in the broadcast function.
|
// Client tracking is done dynamically in the broadcast function.
|
||||||
@@ -733,119 +859,140 @@ static esp_err_t api_ota_upload_handler(httpd_req_t *req) {
|
|||||||
// URI Registration
|
// 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_ws_serial_handler, ws_serial_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 = {
|
static const httpd_uri_t uri_index = {
|
||||||
.uri = "/",
|
.uri = "/",
|
||||||
.method = HTTP_GET,
|
.method = HTTP_GET,
|
||||||
.handler = index_handler,
|
.handler = auth_index_handler,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const httpd_uri_t uri_api_gpio = {
|
static const httpd_uri_t uri_api_gpio = {
|
||||||
.uri = "/api/gpio",
|
.uri = "/api/gpio",
|
||||||
.method = HTTP_GET,
|
.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 = {
|
static const httpd_uri_t uri_api_gpio_set = {
|
||||||
.uri = "/api/gpio/set",
|
.uri = "/api/gpio/set",
|
||||||
.method = HTTP_POST,
|
.method = HTTP_POST,
|
||||||
.handler = api_gpio_set_handler,
|
.handler = auth_api_gpio_set_handler,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const httpd_uri_t uri_api_gpio_config = {
|
static const httpd_uri_t uri_api_gpio_config = {
|
||||||
.uri = "/api/gpio/config",
|
.uri = "/api/gpio/config",
|
||||||
.method = HTTP_PUT,
|
.method = HTTP_PUT,
|
||||||
.handler = api_gpio_config_handler,
|
.handler = auth_api_gpio_config_handler,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const httpd_uri_t uri_api_power_status = {
|
static const httpd_uri_t uri_api_power_status = {
|
||||||
.uri = "/api/power/status",
|
.uri = "/api/power/status",
|
||||||
.method = HTTP_GET,
|
.method = HTTP_GET,
|
||||||
.handler = api_power_status_handler,
|
.handler = auth_api_power_status_handler,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const httpd_uri_t uri_api_power_on = {
|
static const httpd_uri_t uri_api_power_on = {
|
||||||
.uri = "/api/power/on",
|
.uri = "/api/power/on",
|
||||||
.method = HTTP_POST,
|
.method = HTTP_POST,
|
||||||
.handler = api_power_on_handler,
|
.handler = auth_api_power_on_handler,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const httpd_uri_t uri_api_power_off = {
|
static const httpd_uri_t uri_api_power_off = {
|
||||||
.uri = "/api/power/off",
|
.uri = "/api/power/off",
|
||||||
.method = HTTP_POST,
|
.method = HTTP_POST,
|
||||||
.handler = api_power_off_handler,
|
.handler = auth_api_power_off_handler,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const httpd_uri_t uri_api_power_reset = {
|
static const httpd_uri_t uri_api_power_reset = {
|
||||||
.uri = "/api/power/reset",
|
.uri = "/api/power/reset",
|
||||||
.method = HTTP_POST,
|
.method = HTTP_POST,
|
||||||
.handler = api_power_reset_handler,
|
.handler = auth_api_power_reset_handler,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const httpd_uri_t uri_api_serial_config_get = {
|
static const httpd_uri_t uri_api_serial_config_get = {
|
||||||
.uri = "/api/serial/config",
|
.uri = "/api/serial/config",
|
||||||
.method = HTTP_GET,
|
.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 = {
|
static const httpd_uri_t uri_api_serial_config_set = {
|
||||||
.uri = "/api/serial/config",
|
.uri = "/api/serial/config",
|
||||||
.method = HTTP_PUT,
|
.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 = {
|
static const httpd_uri_t uri_api_serial_send = {
|
||||||
.uri = "/api/serial/send",
|
.uri = "/api/serial/send",
|
||||||
.method = HTTP_POST,
|
.method = HTTP_POST,
|
||||||
.handler = api_serial_send_handler,
|
.handler = auth_api_serial_send_handler,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const httpd_uri_t uri_ws_serial = {
|
static const httpd_uri_t uri_ws_serial = {
|
||||||
.uri = "/api/serial/ws",
|
.uri = "/api/serial/ws",
|
||||||
.method = HTTP_GET,
|
.method = HTTP_GET,
|
||||||
.handler = ws_serial_handler,
|
.handler = auth_ws_serial_handler,
|
||||||
.is_websocket = true,
|
.is_websocket = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const httpd_uri_t uri_api_system_info = {
|
static const httpd_uri_t uri_api_system_info = {
|
||||||
.uri = "/api/system/info",
|
.uri = "/api/system/info",
|
||||||
.method = HTTP_GET,
|
.method = HTTP_GET,
|
||||||
.handler = api_system_info_handler,
|
.handler = auth_api_system_info_handler,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const httpd_uri_t uri_api_system_status = {
|
static const httpd_uri_t uri_api_system_status = {
|
||||||
.uri = "/api/system/status",
|
.uri = "/api/system/status",
|
||||||
.method = HTTP_GET,
|
.method = HTTP_GET,
|
||||||
.handler = api_system_status_handler,
|
.handler = auth_api_system_status_handler,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const httpd_uri_t uri_api_ota_status = {
|
static const httpd_uri_t uri_api_ota_status = {
|
||||||
.uri = "/api/ota/status",
|
.uri = "/api/ota/status",
|
||||||
.method = HTTP_GET,
|
.method = HTTP_GET,
|
||||||
.handler = api_ota_status_handler,
|
.handler = auth_api_ota_status_handler,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const httpd_uri_t uri_api_ota_update = {
|
static const httpd_uri_t uri_api_ota_update = {
|
||||||
.uri = "/api/ota/update",
|
.uri = "/api/ota/update",
|
||||||
.method = HTTP_POST,
|
.method = HTTP_POST,
|
||||||
.handler = api_ota_update_handler,
|
.handler = auth_api_ota_update_handler,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const httpd_uri_t uri_api_ota_upload = {
|
static const httpd_uri_t uri_api_ota_upload = {
|
||||||
.uri = "/api/ota/upload",
|
.uri = "/api/ota/upload",
|
||||||
.method = HTTP_POST,
|
.method = HTTP_POST,
|
||||||
.handler = api_ota_upload_handler,
|
.handler = auth_api_ota_upload_handler,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const httpd_uri_t uri_api_logs_get = {
|
static const httpd_uri_t uri_api_logs_get = {
|
||||||
.uri = "/api/logs",
|
.uri = "/api/logs",
|
||||||
.method = HTTP_GET,
|
.method = HTTP_GET,
|
||||||
.handler = api_logs_get_handler,
|
.handler = auth_api_logs_get_handler,
|
||||||
};
|
};
|
||||||
|
|
||||||
static const httpd_uri_t uri_api_logs_clear = {
|
static const httpd_uri_t uri_api_logs_clear = {
|
||||||
.uri = "/api/logs/clear",
|
.uri = "/api/logs/clear",
|
||||||
.method = HTTP_POST,
|
.method = HTTP_POST,
|
||||||
.handler = api_logs_clear_handler,
|
.handler = auth_api_logs_clear_handler,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user