Files
ink-board/main/ui/apps/iotdis/web/web_handlers.cpp

363 lines
12 KiB
C++

#include "web_handlers.h"
#include "../app.h"
#include "esp_log.h"
#include "network/network.h"
#include "common/system_context.h"
#include "esp_random.h"
#include <sstream>
#include <iomanip>
static const char* TAG = "DiscordWebHandler";
WebHandler::~WebHandler() {
stop_web_server();
}
esp_err_t WebHandler::start_web_server() {
if (web_server_ && web_server_->is_running()) {
ESP_LOGI(TAG, "Web server already running");
return ESP_OK;
}
auth_key_ = generate_auth_key_();
uint16_t port = web_server_->start(
auth_key_,
8080
);
if (port == 0) {
ESP_LOGE(TAG, "Failed to start web server");
return ESP_FAIL;
}
esp_err_t ret = register_web_endpoints_();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to register web endpoints");
web_server_->stop();
return ret;
}
ESP_LOGI(TAG, "Web server started");
return ESP_OK;
}
esp_err_t WebHandler::stop_web_server() {
if (web_server_) {
web_server_->stop();
ESP_LOGI(TAG, "Web server stopped");
}
auth_key_.clear();
return ESP_OK;
}
std::string WebHandler::get_url() const {
if (web_server_ && web_server_->is_running()) {
NetworkHandler* network_handler = SystemContext::instance().get_network_handler();
if (!network_handler) {
ESP_LOGE(TAG, "Network handler not available in system context");
return "";
}
WifiHandler& wifi_handler = network_handler->get_wifi_handler();
std::string device_ip = wifi_handler.get_current_ip();
if (device_ip.empty()) {
ESP_LOGW(TAG, "Device not connected to WiFi");
return "";
}
uint16_t port = web_server_->get_port();
std::ostringstream url;
url << "http://" << device_ip << ":" << port << "/?auth=" << auth_key_;
return url.str();
}
return "";
}
std::string WebHandler::get_device_ip() const {
if (web_server_ && web_server_->is_running()) {
NetworkHandler* network_handler = SystemContext::instance().get_network_handler();
if (!network_handler) {
ESP_LOGE(TAG, "Network handler not available in system context");
return "";
}
WifiHandler& wifi_handler = network_handler->get_wifi_handler();
return wifi_handler.get_current_ip();
}
return "";
}
uint16_t WebHandler::get_port() const {
if (web_server_ && web_server_->is_running()) {
return web_server_->get_port();
}
return 0;
}
//
//
//
std::string WebHandler::generate_auth_key_() {
// Generate 128-bit random key using ESP32 hardware RNG
uint32_t rand_values[4];
for (int i = 0; i < 4; i++) {
rand_values[i] = esp_random();
}
// Convert to hex string
std::ostringstream oss;
oss << std::hex << std::setfill('0');
for (int i = 0; i < 4; i++) {
oss << std::setw(8) << rand_values[i];
}
return oss.str();
}
esp_err_t WebHandler::register_web_endpoints_() {
if (!web_server_ || !web_server_->is_running()) {
return ESP_FAIL;
}
// GET / - Serve settings page
httpd_uri_t settings_page_uri = {
.uri = "/",
.method = HTTP_GET,
.handler = settings_page_handler_,
.user_ctx = this
};
web_server_->register_uri_handler(&settings_page_uri);
// POST /save - Save settings
httpd_uri_t save_settings_uri = {
.uri = "/save",
.method = HTTP_POST,
.handler = save_settings_handler_,
.user_ctx = this
};
web_server_->register_uri_handler(&save_settings_uri);
// POST /test - Test connection
httpd_uri_t test_connection_uri = {
.uri = "/test",
.method = HTTP_POST,
.handler = test_connection_handler_,
.user_ctx = this
};
web_server_->register_uri_handler(&test_connection_uri);
return ESP_OK;
}
esp_err_t WebHandler::settings_page_handler_(httpd_req_t* req) {
WebHandler* self = static_cast<WebHandler*>(req->user_ctx);
// Validate auth
size_t query_len = httpd_req_get_url_query_len(req);
if (query_len == 0) {
httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
return ESP_FAIL;
}
char* query = new char[query_len + 1];
if (httpd_req_get_url_query_str(req, query, query_len + 1) != ESP_OK) {
delete[] query;
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Bad Request");
return ESP_FAIL;
}
if (!self->web_server_->validate_auth(query)) {
delete[] query;
httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
return ESP_FAIL;
}
delete[] query;
// Get current settings (access private members via friend)
std::string current_ip = self->setting_handler_->get_remote_ip();
uint16_t current_port = self->setting_handler_->get_remote_port();
uint16_t current_local_port = self->setting_handler_->get_local_port();
// Build HTML page
std::ostringstream html;
html << "<!DOCTYPE html><html><head>"
<< "<meta name='viewport' content='width=device-width, initial-scale=1'>"
<< "<title>Discord Bridge Settings</title>"
<< "<style>"
<< "body{font-family:Arial,sans-serif;max-width:600px;margin:50px auto;padding:20px;}"
<< "h1{color:#333;}"
<< "label{display:block;margin-top:15px;font-weight:bold;}"
<< "input{width:100%;padding:10px;margin-top:5px;box-sizing:border-box;font-size:16px;}"
<< "button{width:100%;padding:12px;margin-top:20px;font-size:16px;cursor:pointer;}"
<< ".btn-primary{background:#4CAF50;color:white;border:none;}"
<< ".btn-secondary{background:#008CBA;color:white;border:none;}"
<< "#result{margin-top:20px;padding:10px;border-radius:5px;display:none;}"
<< ".success{background:#d4edda;color:#155724;border:1px solid #c3e6cb;}"
<< ".error{background:#f8d7da;color:#721c24;border:1px solid #f5c6cb;}"
<< "</style></head><body>"
<< "<h1>Discord Bridge Settings</h1>"
<< "<form id='settingsForm'>"
<< "<label for='ip'>Bridge IP Address:</label>"
<< "<input type='text' id='ip' name='ip' placeholder='e.g., 192.168.1.100' value='" << current_ip << "' required>"
<< "<label for='port'>Bridge Port:</label>"
<< "<input type='number' id='port' name='port' placeholder='e.g., 4211' value='" << current_port << "' required min='1' max='65535'>"
<< "<label for='localPort'>ESP32 Local Port:</label>"
<< "<input type='number' id='localPort' name='localPort' placeholder='e.g., 4212' value='" << current_local_port << "' required min='1' max='65535'>"
<< "<button type='button' class='btn-secondary' onclick='testConnection()'>Test Connection</button>"
<< "<button type='submit' class='btn-primary'>Save Settings</button>"
<< "</form>"
<< "<div id='result'></div>"
<< "<script>"
<< "const form=document.getElementById('settingsForm');"
<< "const result=document.getElementById('result');"
<< "function showResult(msg,isSuccess){"
<< "result.textContent=msg;"
<< "result.className=isSuccess?'success':'error';"
<< "result.style.display='block';"
<< "}"
<< "function testConnection(){"
<< "const ip=document.getElementById('ip').value;"
<< "const port=document.getElementById('port').value;"
<< "const localPort=document.getElementById('localPort').value;"
<< "if(!ip||!port||!localPort){showResult('Please fill all fields',false);return;}"
<< "showResult('Testing connection...',false);"
<< "fetch('/test',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},"
<< "body:'ip='+encodeURIComponent(ip)+'&port='+encodeURIComponent(port)+'&localPort='+encodeURIComponent(localPort)})"
<< ".then(r=>r.json()).then(data=>showResult(data.message,data.success))"
<< ".catch(()=>showResult('Request failed',false));"
<< "}"
<< "form.addEventListener('submit',function(e){"
<< "e.preventDefault();"
<< "const ip=document.getElementById('ip').value;"
<< "const port=document.getElementById('port').value;"
<< "const localPort=document.getElementById('localPort').value;"
<< "if(!ip||!port||!localPort){showResult('Please fill all fields',false);return;}"
<< "fetch('/save',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},"
<< "body:'ip='+encodeURIComponent(ip)+'&port='+encodeURIComponent(port)+'&localPort='+encodeURIComponent(localPort)})"
<< ".then(r=>r.json()).then(data=>{showResult(data.message,data.success);"
<< "if(data.success)setTimeout(()=>result.style.display='none',3000);})"
<< ".catch(()=>showResult('Request failed',false));"
<< "});"
<< "</script></body></html>";
std::string html_str = html.str();
httpd_resp_set_type(req, "text/html");
httpd_resp_send(req, html_str.c_str(), html_str.length());
return ESP_OK;
}
esp_err_t WebHandler::save_settings_handler_(httpd_req_t* req) {
WebHandler* self = static_cast<WebHandler*>(req->user_ctx);
// Read POST data
char* buf = new char[req->content_len + 1];
int ret = httpd_req_recv(req, buf, req->content_len);
if (ret <= 0) {
delete[] buf;
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Failed to read request");
return ESP_FAIL;
}
buf[ret] = '\0';
// Parse form data
char ip[64] = { 0 };
char port_str[8] = { 0 };
char local_port_str[8] = { 0 };
httpd_query_key_value(buf, "ip", ip, sizeof(ip));
httpd_query_key_value(buf, "port", port_str, sizeof(port_str));
httpd_query_key_value(buf, "localPort", local_port_str, sizeof(local_port_str));
delete[] buf;
if (strlen(ip) == 0 || strlen(port_str) == 0 || strlen(local_port_str) == 0) {
const char* resp = "{\"success\":false,\"message\":\"Missing fields\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
return ESP_OK;
}
uint16_t port = static_cast<uint16_t>(atoi(port_str));
uint16_t local_port = static_cast<uint16_t>(atoi(local_port_str));
if (port == 0 || local_port == 0) {
const char* resp = "{\"success\":false,\"message\":\"Invalid port\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
return ESP_OK;
}
// Save settings
if (self && self->setting_handler_) {
self->setting_handler_->save_settings(std::string(ip), port, local_port);
}
const char* resp = "{\"success\":true,\"message\":\"Settings saved successfully\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
ESP_LOGI(TAG, "Settings saved via web interface: %s:%u (local port: %u)", ip, port, local_port);
return ESP_OK;
}
esp_err_t WebHandler::test_connection_handler_(httpd_req_t* req) {
WebHandler* self = static_cast<WebHandler*>(req->user_ctx);
IotDisBridge* bridge = self ? self->bridge_ : nullptr;
// Read POST data
char* buf = new char[req->content_len + 1];
int ret = httpd_req_recv(req, buf, req->content_len);
if (ret <= 0) {
delete[] buf;
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Failed to read request");
return ESP_FAIL;
}
buf[ret] = '\0';
// Parse form data
char ip[64] = { 0 };
char port_str[8] = { 0 };
char local_port_str[8] = { 0 };
httpd_query_key_value(buf, "ip", ip, sizeof(ip));
httpd_query_key_value(buf, "port", port_str, sizeof(port_str));
httpd_query_key_value(buf, "localPort", local_port_str, sizeof(local_port_str));
delete[] buf;
if (strlen(ip) == 0 || strlen(port_str) == 0 || strlen(local_port_str) == 0) {
const char* resp = "{\"success\":false,\"message\":\"Missing fields\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
return ESP_OK;
}
uint16_t port = static_cast<uint16_t>(atoi(port_str));
uint16_t local_port = static_cast<uint16_t>(atoi(local_port_str));
if (port == 0 || local_port == 0) {
const char* resp = "{\"success\":false,\"message\":\"Invalid port\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
return ESP_OK;
}
// Test connection
bool success = false;
if (bridge) {
success = bridge->test_connection(std::string(ip), port, local_port);
} else {
ESP_LOGE(TAG, "IotDisBridge pointer is null, cannot test connection");
}
const char* resp = success
? "{\"success\":true,\"message\":\"Connection successful!\"}"
: "{\"success\":false,\"message\":\"No response from bridge\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
ESP_LOGI(TAG, "Connection test via web interface: %s:%u (local port: %u) - %s", ip, port, local_port, success ? "SUCCESS" : "FAILED");
return ESP_OK;
}