/* ; Project: Open Vehicle Monitor System ; Date: 14th March 2017 ; ; Changes: ; 1.0 Initial release ; ; (C) 2011 Michael Stegen / Stegen Electronics ; (C) 2011-2017 Mark Webb-Johnson ; (C) 2011 Sonny Chen @ EPRO/DX ; (C) 2018 Michael Balzer ; ; Permission is hereby granted, free of charge, to any person obtaining a copy ; of this software and associated documentation files (the "Software"), to deal ; in the Software without restriction, including without limitation the rights ; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ; copies of the Software, and to permit persons to whom the Software is ; furnished to do so, subject to the following conditions: ; ; The above copyright notice and this permission notice shall be included in ; all copies or substantial portions of the Software. ; ; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN ; THE SOFTWARE. */ // We're using ESP_EARLY_LOG* (direct USB console output) for protocol debug logging. // To enable debug logging locally, uncomment: // #define LOG_LOCAL_LEVEL ESP_LOG_VERBOSE #include "ovms_log.h" static const char *TAG = "webserver"; #include #include #include #include "ovms_webserver.h" #include "ovms_config.h" #include "ovms_metrics.h" #include "metrics_standard.h" #include "buffered_shell.h" #include "vehicle.h" #ifdef CONFIG_OVMS_COMP_PLUGINS #include "ovms_plugins.h" #endif // #ifdef CONFIG_OVMS_COMP_PLUGINS OvmsWebServer MyWebServer __attribute__ ((init_priority (8200))); OvmsWebServer::OvmsWebServer() { ESP_LOGI(TAG, "Initialising WEBSERVER (8200)"); m_running = false; m_configured = false; m_shutdown_countdown = 0; memset(m_sessions, 0, sizeof(m_sessions)); #if MG_ENABLE_FILESYSTEM m_file_enable = true; memset(&m_file_opts, 0, sizeof(m_file_opts)); #endif //MG_ENABLE_FILESYSTEM m_client_cnt = 0; m_client_mutex = xSemaphoreCreateMutex(); m_client_backlog = xQueueCreate(50, sizeof(WebSocketTxTodo)); m_update_ticker = xTimerCreate("Web client update ticker", 250 / portTICK_PERIOD_MS, pdTRUE, NULL, UpdateTicker); MyConfig.RegisterParam("http.server", "Webserver configuration", true, true); MyConfig.RegisterParam("http.plugin", "Webserver plugins", true, true); #undef bind // Kludgy, but works using std::placeholders::_1; using std::placeholders::_2; MyEvents.RegisterEvent(TAG, "network.mgr.init", std::bind(&OvmsWebServer::NetManInit, this, _1, _2)); MyEvents.RegisterEvent(TAG, "network.mgr.stop", std::bind(&OvmsWebServer::NetManStop, this, _1, _2)); MyEvents.RegisterEvent(TAG, "config.changed", std::bind(&OvmsWebServer::ConfigChanged, this, _1, _2)); MyEvents.RegisterEvent(TAG, "config.mounted", std::bind(&OvmsWebServer::ConfigChanged, this, _1, _2)); MyEvents.RegisterEvent(TAG, "*", std::bind(&OvmsWebServer::EventListener, this, _1, _2)); // register standard framework URIs: RegisterPage("/", "OVMS", HandleRoot); RegisterPage("/assets/style.css", "style.css", HandleAsset); RegisterPage("/assets/script.js", "script.js", HandleAsset); RegisterPage("/assets/charts.js", "charts.js", HandleAsset); RegisterPage("/assets/tables.js", "tables.js", HandleAsset); RegisterPage("/assets/zones.json", "zones.json", HandleAsset); RegisterPage("/assets/bootstrap.min.css.map", "-", HandleAsset); RegisterPage("/favicon.ico", "favicon.ico", HandleAsset); RegisterPage("/apple-touch-icon.png", "apple-touch-icon.png", HandleAsset); RegisterPage("/menu", "Menu", HandleMenu); RegisterPage("/home", "Home", HandleHome); RegisterPage("/login", "Login", HandleLogin); RegisterPage("/logout", "Logout", HandleLogout); // register standard API calls: RegisterPage("/api/execute", "Execute command", HandleCommand, PageMenu_None, PageAuth_Cookie); RegisterPage("/api/file", "Load/Save file", HandleFile, PageMenu_None, PageAuth_Cookie); // register standard public pages: RegisterPage("/dashboard", "Dashboard", HandleDashboard, PageMenu_Main, PageAuth_None); // register standard administration pages: RegisterPage("/status", "Status", HandleStatus, PageMenu_Main, PageAuth_Cookie); RegisterPage("/shell", "Shell", HandleShell, PageMenu_Tools, PageAuth_Cookie); RegisterPage("/edit", "Editor", HandleEditor, PageMenu_Tools, PageAuth_Cookie); #ifdef WEBSRV_HAVE_SETUPWIZARD RegisterPage("/cfg/init", "Setup wizard", HandleCfgInit, PageMenu_None, PageAuth_Cookie); #endif RegisterPage("/cfg/password", "Password", HandleCfgPassword, PageMenu_Config, PageAuth_Cookie); RegisterPage("/cfg/vehicle", "Vehicle", HandleCfgVehicle, PageMenu_Config, PageAuth_Cookie); RegisterPage("/cfg/wifi", "Wifi", HandleCfgWifi, PageMenu_Config, PageAuth_Cookie); #ifdef CONFIG_OVMS_COMP_CELLULAR RegisterPage("/cfg/cellular", "Cellular", HandleCfgModem, PageMenu_Config, PageAuth_Cookie); #endif #ifdef CONFIG_OVMS_COMP_SERVER #ifdef CONFIG_OVMS_COMP_SERVER_V2 RegisterPage("/cfg/server/v2", "Server V2 (MP)", HandleCfgServerV2, PageMenu_Config, PageAuth_Cookie); #endif #ifdef CONFIG_OVMS_COMP_SERVER_V3 RegisterPage("/cfg/server/v3", "Server V3 (MQTT)", HandleCfgServerV3, PageMenu_Config, PageAuth_Cookie); #endif RegisterPage("/cfg/notifications", "Notifications", HandleCfgNotifications, PageMenu_Config, PageAuth_Cookie); #endif #ifdef CONFIG_OVMS_COMP_PUSHOVER RegisterPage("/cfg/pushover", "Pushover", HandleCfgPushover, PageMenu_Config, PageAuth_Cookie); #endif RegisterPage("/cfg/webserver", "Webserver", HandleCfgWebServer, PageMenu_Config, PageAuth_Cookie); RegisterPage("/cfg/plugins", "Web Plugins", HandleCfgPlugins, PageMenu_Config, PageAuth_Cookie); RegisterPage("/cfg/autostart", "Autostart", HandleCfgAutoInit, PageMenu_Config, PageAuth_Cookie); #ifdef CONFIG_OVMS_COMP_OTA RegisterPage("/cfg/firmware", "Firmware", HandleCfgFirmware, PageMenu_Config, PageAuth_Cookie); #endif RegisterPage("/cfg/logging", "Logging", HandleCfgLogging, PageMenu_Config, PageAuth_Cookie); RegisterPage("/cfg/locations", "Locations", HandleCfgLocations, PageMenu_Config, PageAuth_Cookie); RegisterPage("/cfg/backup", "Backup & Restore", HandleCfgBackup, PageMenu_Config, PageAuth_Cookie); } OvmsWebServer::~OvmsWebServer() { // never destroyed } void OvmsWebServer::NetManInit(std::string event, void* data) { m_running = true; ESP_LOGI(TAG,"Launching Web Server"); if (!m_configured) { // safety measure, should not happen normally ConfigChanged("config.mounted", NULL); } struct mg_mgr* mgr = MyNetManager.GetMongooseMgr(); char *error_string; struct mg_bind_opts bind_opts = {}; memset(&bind_opts, 0, sizeof(bind_opts)); bind_opts.error_string = (const char**) &error_string; // bind http: ESP_LOGI(TAG, "Binding to port 80 (http)"); struct mg_connection *nc = mg_bind_opt(mgr, ":80", EventHandler, bind_opts); if (!nc) { ESP_LOGE(TAG, "Cannot bind to port 80 (http): %s", error_string); } else { mg_set_protocol_http_websocket(nc); mg_set_timer(nc, mg_time() + SESSION_CHECK_INTERVAL); } #ifdef CONFIG_MG_ENABLE_SSL // bind https: if (path_exists("/store/tls/webserver.crt") && path_exists("/store/tls/webserver.key")) { ESP_LOGI(TAG, "Binding to port 443 (https)"); bind_opts.ssl_cert = "/store/tls/webserver.crt"; bind_opts.ssl_key = "/store/tls/webserver.key"; nc = mg_bind_opt(mgr, ":443", EventHandler, bind_opts); if (!nc) { ESP_LOGE(TAG, "Cannot bind to port 443 (https): %s", error_string); } else { mg_set_protocol_http_websocket(nc); } } #endif // CONFIG_MG_ENABLE_SSL } void OvmsWebServer::NetManStop(std::string event, void* data) { if (m_running) { ESP_LOGI(TAG,"Stopping Web Server"); m_running = false; } } /** * ConfigChanged: read & apply configuration updates */ void OvmsWebServer::ConfigChanged(std::string event, void* data) { m_configured = true; OvmsConfigParam* param = (OvmsConfigParam*) data; ESP_LOGD(TAG, "ConfigChanged: %s %s", event.c_str(), param ? param->GetName().c_str() : ""); #ifdef WEBSRV_HAVE_SETUPWIZARD if (event == "config.mounted") { CfgInitStartup(); } #endif #if MG_ENABLE_FILESYSTEM if (!param || param->GetName() == "http.server") { // Instances: // Name Default Function // enable.files yes Enable file serving from docroot // enable.dirlist yes Enable directory listings // docroot /sd File server document root // auth.domain ovms Default auth domain (digest realm) // auth.file .htpasswd Per directory auth file (Note: no inheritance from parent dir!) // auth.global yes Use global auth for files (user "admin", module password) if (m_file_opts.document_root) free((void*)m_file_opts.document_root); if (m_file_opts.auth_domain) free((void*)m_file_opts.auth_domain); if (m_file_opts.per_directory_auth_file) free((void*)m_file_opts.per_directory_auth_file); m_file_enable = MyConfig.GetParamValueBool("http.server", "enable.files", true); m_file_opts.enable_directory_listing = MyConfig.GetParamValueBool("http.server", "enable.dirlist", true) ? "yes" : "no"; m_file_opts.document_root = strdup(MyConfig.GetParamValue("http.server", "docroot", "/sd").c_str()); m_file_opts.auth_domain = strdup(MyConfig.GetParamValue("http.server", "auth.domain", "ovms").c_str()); m_file_opts.per_directory_auth_file = strdup(MyConfig.GetParamValue("http.server", "auth.file", ".htpasswd").c_str()); m_file_opts.global_auth_file = MyConfig.GetParamValueBool("http.server", "auth.global", true) ? OVMS_GLOBAL_AUTH_FILE : NULL; } if (!param || param->GetName() == "password") { UpdateGlobalAuthFile(); } #endif //MG_ENABLE_FILESYSTEM if (!param || param->GetName() == "http.plugin") { DeregisterPlugins(); RegisterPlugins(); } } /** * UpdateGlobalAuthFile: create digest auth for main user "admin" * if password is set and global auth is activated */ void OvmsWebServer::UpdateGlobalAuthFile() { #if MG_ENABLE_FILESYSTEM if (!m_file_opts.global_auth_file || !m_file_opts.auth_domain) return; std::string p = MyConfig.GetParamValue("password", "module"); if (p.empty()) { unlink(m_file_opts.global_auth_file); ESP_LOGW(TAG, "UpdateGlobalAuthFile: no password set => no auth for web console"); } else { FILE *fp = fopen(m_file_opts.global_auth_file, "w"); if (!fp) { ESP_LOGE(TAG, "UpdateGlobalAuthFile: can't write to '%s'", m_file_opts.global_auth_file); return; } std::string auth = MakeDigestAuth(m_file_opts.auth_domain, "admin", p.c_str()); fprintf(fp, "%s\n", auth.c_str()); ESP_LOGD(TAG, "UpdateGlobalAuthFile: 'admin' => %s", auth.c_str()); auth = MakeDigestAuth(m_file_opts.auth_domain, "", p.c_str()); fprintf(fp, "%s\n", auth.c_str()); ESP_LOGD(TAG, "UpdateGlobalAuthFile: '' => %s", auth.c_str()); if (fclose(fp) != 0) ESP_LOGE(TAG, "UpdateGlobalAuthFile: can't write to '%s': %s", m_file_opts.global_auth_file, strerror(errno)); } #endif //MG_ENABLE_FILESYSTEM } const std::string OvmsWebServer::MakeDigestAuth(const char* realm, const char* username, const char* password) { std::string line; line = username; line += ":"; line += realm; line += ":"; line += password; unsigned char digest[16]; cs_md5_ctx md5_ctx; cs_md5_init(&md5_ctx); cs_md5_update(&md5_ctx, (const unsigned char*) line.data(), line.length()); cs_md5_final(digest, &md5_ctx); char hex[33]; cs_to_hex(hex, digest, sizeof(digest)); line = username; line += ":"; line += realm; line += ":"; line += hex; return line; } /** * ExecuteCommand: BufferedShell wrapper */ const std::string OvmsWebServer::ExecuteCommand(const std::string command, int verbosity /*=COMMAND_RESULT_NORMAL*/) { // Note: assuming user is admin (secure=true) return BufferedShell::ExecuteCommand(command, true, verbosity); } /** * RegisterPage: add a page to the URI handler map * Note: use PageMenu_Vehicle for vehicle specific pages. */ void OvmsWebServer::RegisterPage(std::string uri, std::string label, PageHandler_t handler, PageMenu_t menu /*=PageMenu_None*/, PageAuth_t auth /*=PageAuth_None*/, int priority /*=0*/) { auto prev = m_pagemap.before_begin(); for (auto it = m_pagemap.begin(); it != m_pagemap.end(); prev = it++) { if ((*it).uri == uri) { if (priority < 0) { ESP_LOGE(TAG, "RegisterPage: second registration for uri '%s' (ignored)", uri.c_str()); return; } else { ESP_LOGI(TAG, "RegisterPage: second registration for uri '%s' prioritized", uri.c_str()); m_pagemap.erase_after(prev); break; } } } m_pagemap.insert_after(prev, PageEntry(uri, label, handler, menu, auth)); } void OvmsWebServer::DeregisterPage(std::string uri) { m_pagemap.remove_if([uri](PageEntry &e){ return (e.uri == uri); }); } PageEntry* OvmsWebServer::FindPage(std::string uri) { for (PageEntry& e : m_pagemap) { if (e.uri == uri) return &e; } return NULL; } /** * Plugin Registry */ void PagePluginContent::LoadContent() { std::string path; if (m_pluginstore) { path = "/store/plugins/" + m_path; } else { path = "/store/plugin/" + m_path; } std::ifstream file(path, std::ios::in | std::ios::binary | std::ios::ate); ESP_LOGD(TAG,"Plugin LoadContent: %s = %s",m_path.c_str(),path.c_str()); if (!file.is_open()) { ESP_LOGE(TAG, "Plugin file missing: '%s'", path.c_str()); m_content = ""; } else { auto size = file.tellg(); if (size < 0) { ESP_LOGE(TAG, "Plugin file '%s': seek failed", path.c_str()); m_content = ""; } else { m_content.resize(size, '\0'); file.seekg(0); file.read(&m_content[0], size); ESP_LOGD(TAG, "Plugin file loaded: '%s', %u bytes", path.c_str(), (size_t)size); } } } void OvmsWebServer::RegisterPlugins() { OvmsConfigParam* cp = MyConfig.CachedParam("http.plugin"); if (!cp) return; const ConfigParamMap& pmap = cp->GetMap(); std::string key, label, page, hook; PageAuth_t auth; PageMenu_t menu; // Instances: // .enable yes/no // .label e.g. "My Plugin" // .page e.g. "/myplugin" (page/hook URI) // .auth [page] code name // .menu [page] code name // .hook [hook] Callback hook code // // Files: // /store/plugin/ for (auto& kv: pmap) { if (!endsWith(kv.first, ".enable") || !strtobool(kv.second)) continue; key = kv.first.substr(0, kv.first.length() - 7); page = cp->GetValue(key+".page"); hook = cp->GetValue(key+".hook"); bool is_hook = cp->IsDefined(key+".hook"); if (page == "" || (is_hook && hook == "")) continue; if (is_hook) { m_plugin_parts.insert({ page + ":" + hook, PagePluginContent(key) }); RegisterCallback("http.plugin", page, PluginCallback, -1); ESP_LOGD(TAG, "Plugin hook registered: '%s:%s' => '%s'", page.c_str(), hook.c_str(), key.c_str()); } else { m_plugin_pages.insert({ page, PagePluginContent(key) }); label = cp->GetValue(key+".label"); menu = Code2PageMenu(cp->GetValue(key+".menu")); auth = Code2PageAuth(cp->GetValue(key+".auth")); RegisterPage(page, label, PluginHandler, menu, auth, -1); ESP_LOGD(TAG, "Plugin page registered: '%s' => '%s'", page.c_str(), key.c_str()); } } #ifdef CONFIG_OVMS_COMP_PLUGINS // Plugins MyPluginStore.LoadEnabledModules(EL_WEB_PAGE); MyPluginStore.LoadEnabledModules(EL_WEB_HOOK); #endif // #ifdef CONFIG_OVMS_COMP_PLUGIN } void OvmsWebServer::DeregisterPlugins() { m_pagemap.remove_if([](PageEntry& e){ return e.handler == PluginHandler; }); m_plugin_pages.clear(); DeregisterCallbacks("http.plugin"); m_plugin_parts.clear(); } void OvmsWebServer::ReloadPlugin(std::string path) { if (startsWith(path, "/store/plugin/")) path = path.substr(14); else if (startsWith(path, "/store/plugins/")) path = path.substr(15); for (auto i = m_plugin_pages.begin(); i != m_plugin_pages.end(); i++) { if (i->second.m_path == path) i->second.LoadContent(); } for (auto i = m_plugin_parts.begin(); i != m_plugin_parts.end(); i++) { if (i->second.m_path == path) i->second.LoadContent(); } } void OvmsWebServer::PluginHandler(PageEntry_t& p, PageContext_t& c) { auto i = MyWebServer.m_plugin_pages.find(p.uri); if (i == MyWebServer.m_plugin_pages.end()) return; extram::string& content = i->second.GetContent(); c.head(200); c.print(content); c.done(); } PageResult_t OvmsWebServer::PluginCallback(PageEntry_t& p, PageContext_t& c, const std::string& hook) { std::string key = p.uri + ':' + hook; auto range = MyWebServer.m_plugin_parts.equal_range(key); if (range.first == MyWebServer.m_plugin_parts.end()) return PageResult_OK; for (auto i = range.first; i != range.second; i++) { extram::string& content = i->second.GetContent(); c.print(content); } return PageResult_OK; } /** * EventHandler: this is the mongoose main event handler. */ void OvmsWebServer::EventHandler(mg_connection *nc, int ev, void *p) { PageContext_t c; MgHandler* handler = (MgHandler*) nc->user_data; //if (ev != MG_EV_POLL && ev != MG_EV_SEND) //if (nc->user_data) // ESP_EARLY_LOGV(TAG, "EventHandler: conn=%p handler=%p ev=%d p=%p rxbufsz=%d, txbufsz=%d", nc, nc->user_data, ev, p, nc->recv_mbuf.size, nc->send_mbuf.size); // call attached handler: if (handler) ev = handler->HandleEvent(ev, p); // framework handling: switch (ev) { case MG_EV_WEBSOCKET_HANDSHAKE_DONE: // new websocket connection { MyWebServer.CreateWebSocketHandler(nc); } break; case MG_EV_HTTP_REQUEST: // standard HTTP request { c.nc = nc; c.hm = (struct http_message *) p; c.session = MyWebServer.GetSession(c.hm); c.method.assign(c.hm->method.p, c.hm->method.len); c.uri.assign(c.hm->uri.p, c.hm->uri.len); ESP_LOGI(TAG, "HTTP %s %s", c.method.c_str(), c.uri.c_str()); PageEntry* page = MyWebServer.FindPage(c.uri.c_str()); if (page) { // serve by page handler: page->Serve(c); } #if MG_ENABLE_FILESYSTEM else if (MyWebServer.m_file_enable) { // serve from file space: if (MyConfig.ProtectedPath(c.uri)) { mg_http_send_error(c.nc, 401, "Unauthorized"); nc->flags |= MG_F_SEND_AND_CLOSE; } else { mg_serve_http(nc, c.hm, MyWebServer.m_file_opts); } } #endif //MG_ENABLE_FILESYSTEM else { mg_http_send_error(c.nc, 404, "Not found"); nc->flags |= MG_F_SEND_AND_CLOSE; } } break; case MG_EV_CLOSE: // connection has been closed { if (handler) { if (nc->flags & MG_F_IS_WEBSOCKET) MyWebServer.DestroyWebSocketHandler((WebSocketHandler*)handler); else delete handler; } } break; case MG_EV_TIMER: { // Perform session maintenance: MyWebServer.CheckSessions(); mg_set_timer(nc, mg_time() + SESSION_CHECK_INTERVAL); } break; default: break; } } /** * PageEntry.Serve: check auth, call page handler */ void PageEntry::Serve(PageContext_t& c) { // check auth: if #if MG_ENABLE_FILESYSTEM (auth == PageAuth_Cookie && !MyConfig.GetParamValue("password", "module").empty()) #else (auth != PageAuth_None && !MyConfig.GetParamValue("password", "module").empty()) #endif { // use session cookie / access based auth: if (c.session == NULL) { // Check per access auth: // Note: apikey is currently just the admin password, but can later be replaced by an auth token std::string apikey = c.getvar("apikey"); if (!MyWebServer.CheckLogin("admin", apikey)) { if (apikey != "") { // output API response: c.head(403); c.print("ERROR: Unauthorized\n"); c.done(); } else { // forward user to login page: MyWebServer.HandleLogin(*this, c); } return; } } } #if MG_ENABLE_FILESYSTEM else if (auth == PageAuth_File) { // use mongoose htaccess file based digest auth: mg_serve_http_opts& opts = MyWebServer.m_file_opts; if (!mg_http_is_authorized(c.hm, c.hm->uri, opts.auth_domain, opts.global_auth_file, MG_AUTH_FLAG_IS_GLOBAL_PASS_FILE|MG_AUTH_FLAG_ALLOW_MISSING_FILE)) { mg_http_send_digest_auth_request(c.nc, opts.auth_domain); return; } } #endif //MG_ENABLE_FILESYSTEM // call page handler: handler(*this, c); } /** * Page Callback Registry: */ bool OvmsWebServer::RegisterCallback(std::string caller, std::string uri, PageCallback_t handler, int priority /*=0*/) { PageEntry* e = FindPage(uri); if (!e) return false; e->RegisterCallback(caller, handler, priority); return true; } void OvmsWebServer::DeregisterCallbacks(std::string caller) { for (PageEntry& e : m_pagemap) e.DeregisterCallback(caller); } void PageEntry::RegisterCallback(std::string caller, PageCallback_t handler, int priority /*=0*/) { auto prev = callbacklist.before_begin(); for (auto it = callbacklist.begin(); it != callbacklist.end(); prev = it++) { if ((*it).caller == caller && (*it).handler == handler) return; // already registered } if (priority < 0) callbacklist.insert_after(prev, PageCallbackEntry(caller, handler)); else callbacklist.push_front(PageCallbackEntry(caller, handler)); } void PageEntry::DeregisterCallback(std::string caller) { callbacklist.remove_if([caller](PageCallbackEntry& e){ return e.caller == caller; }); } PageResult_t PageEntry::callback(PageContext_t& c, const std::string& hook) { PageResult_t res = PageResult_OK; for (PageCallbackEntry& e : callbacklist) { res = e.handler(*this, c, hook); if (res == PageResult_STOP) break; } return res; } PageResult_t PageContext::callback(PageEntry_t& p, const std::string& hook) { return p.callback(*this, hook); } /** * MgHandler.RequestPoll: init transmission from other context. * * mg_broadcast() signals the mg_mgr_poll() task to send an MG_EV_POLL to all connections. */ void MgHandler::RequestPoll() { #if MG_ENABLE_BROADCAST && WEBSRV_USE_MG_BROADCAST if (!m_nc) return; if (xTaskGetCurrentTaskHandle() == MyNetManager.GetMongooseTaskHandle()) { // we're in the NetManTask, can send directly: HandleEvent(MG_EV_POLL, NULL); } else { MgHandler* origin = this; mg_broadcast(MyNetManager.GetMongooseMgr(), HandlePoll, &origin, sizeof(origin)); } #endif // MG_ENABLE_BROADCAST && WEBSRV_USE_MG_BROADCAST } void MgHandler::HandlePoll(mg_connection* nc, int ev, void* p) { MgHandler* origin = *((MgHandler**)p); if (nc->user_data == origin) origin->HandleEvent(MG_EV_POLL, NULL); } /** * HttpDataSender: chunked transfer of a memory region (needs to be const during xfer) */ HttpDataSender::HttpDataSender(mg_connection* nc, MemRegionList xferlist, bool keepalive /*=true*/) : MgHandler(nc) { m_xferlist = xferlist; m_xfer = -1; m_data = NULL; m_size = 0; m_sent = 0; m_keepalive = keepalive; ESP_EARLY_LOGV(TAG, "HttpDataSender[%p]: init %d regions", nc, m_xferlist.size()); } HttpDataSender::HttpDataSender(mg_connection* nc, const uint8_t* data, size_t size, bool keepalive /*=true*/) : MgHandler(nc) { m_xferlist.push_back({ data, size }); m_xfer = -1; m_data = NULL; m_size = 0; m_sent = 0; m_keepalive = keepalive; ESP_EARLY_LOGV(TAG, "HttpDataSender[%p]: init data=%p, %d bytes", nc, data, size); } HttpDataSender::~HttpDataSender() { if (m_sent < m_size || m_xfer < m_xferlist.size()) { ESP_EARLY_LOGV(TAG, "HttpDataSender[%p]: abort region=%d/%d, data=%p, %d/%d bytes sent", m_nc, m_xfer+1, m_xferlist.size(), m_data, m_sent, m_size); } } int HttpDataSender::HandleEvent(int ev, void* p) { switch (ev) { case MG_EV_SEND: // last transmission has finished { if (m_sent == m_size && ++m_xfer < m_xferlist.size()) { // send next region: m_data = m_xferlist[m_xfer].first; m_size = m_xferlist[m_xfer].second; m_sent = 0; ESP_EARLY_LOGV(TAG, "HttpDataSender[%p]: next region=%d data=%p, %d bytes", m_nc, m_xfer+1, m_data, m_size); } if (m_sent < m_size) { // send next chunk: size_t len = MIN(m_size - m_sent, XFER_CHUNK_SIZE); mg_send_http_chunk(m_nc, (const char*) m_data + m_sent, len); m_sent += len; ESP_EARLY_LOGV(TAG, "HttpDataSender[%p] data=%p sent %d/%d", m_nc, m_data, m_sent, m_size); } else { // done: if (!m_keepalive) m_nc->flags |= MG_F_SEND_AND_CLOSE; mg_send_http_chunk(m_nc, "", 0); ESP_EARLY_LOGV(TAG, "HttpDataSender[%p]: done", m_nc); delete this; } } break; default: break; } return ev; } /** * HttpStringSender: chunked transfer of a memory region (needs to be const during xfer) */ HttpStringSender::HttpStringSender(mg_connection* nc, std::string* msg, bool keepalive /*=true*/) : MgHandler(nc) { m_msg = msg; m_sent = 0; m_keepalive = keepalive; ESP_EARLY_LOGV(TAG, "HttpStringSender[%p]: init msg=%p, %d bytes", nc, m_msg, m_msg->size()); } HttpStringSender::~HttpStringSender() { if (m_sent < m_msg->size()) { ESP_EARLY_LOGV(TAG, "HttpStringSender[%p]: abort msg=%p, %d bytes sent", m_nc, m_msg, m_sent); } delete m_msg; } int HttpStringSender::HandleEvent(int ev, void* p) { switch (ev) { case MG_EV_SEND: // last transmission has finished { if (m_sent < m_msg->size()) { // send next chunk: size_t len = MIN(m_msg->size() - m_sent, XFER_CHUNK_SIZE); mg_send_http_chunk(m_nc, (const char*) m_msg->data() + m_sent, len); m_sent += len; ESP_EARLY_LOGV(TAG, "HttpStringSender[%p] msg=%p sent %d/%d", m_nc, m_msg, m_sent, m_msg->size()); } else { // done: if (!m_keepalive) m_nc->flags |= MG_F_SEND_AND_CLOSE; mg_send_http_chunk(m_nc, "", 0); ESP_EARLY_LOGV(TAG, "HttpStringSender[%p]: done msg=%p, %d bytes sent", m_nc, m_msg, m_sent); delete this; } } break; default: break; } return ev; } /** * CheckLogin: check username & password * * We use "admin" as a fixed username for now to be able to extend users later on * and be compatible with the file based digest authentication. */ bool OvmsWebServer::CheckLogin(std::string username, std::string password) { std::string adminpass = MyConfig.GetParamValue("password", "module"); return (adminpass.empty() || (username == "admin" && password == adminpass)); } /** * GetSession: parses the session cookie and returns a pointer to the session struct * or NULL if not found. */ user_session* OvmsWebServer::GetSession(http_message *hm) { user_session* session = NULL; struct mg_str *cookie_header = mg_get_http_header(hm, "cookie"); if (cookie_header == NULL) return NULL; char ssid_buf[21], *ssid = ssid_buf; mg_http_parse_header2(cookie_header, SESSION_COOKIE_NAME, &ssid, sizeof(ssid_buf)); uint64_t sid = strtoull(ssid, NULL, 16); if (sid != 0) { for (int i = 0; i < NUM_SESSIONS; i++) { if (m_sessions[i].id == sid) { m_sessions[i].last_used = mg_time(); session = &m_sessions[i]; break; } } } if (ssid != ssid_buf) free(ssid); return session; } /** * CreateSession: create a new session */ user_session* OvmsWebServer::CreateSession(const http_message *hm) { /* Find first available slot or use the oldest one. */ user_session *s = NULL; user_session *oldest_s = m_sessions; for (int i = 0; i < NUM_SESSIONS; i++) { if (m_sessions[i].id == 0) { s = &m_sessions[i]; break; } if (m_sessions[i].last_used < oldest_s->last_used) { oldest_s = &m_sessions[i]; } } if (s == NULL) { DestroySession(oldest_s); ESP_LOGD(TAG, "CreateSession: evicted %" INT64_X_FMT, oldest_s->id); s = oldest_s; } /* Initialize new session. */ s->last_used = mg_time(); /* Create an ID by putting various volatiles into a pot and stirring. */ cs_sha1_ctx ctx; cs_sha1_init(&ctx); cs_sha1_update(&ctx, (const unsigned char *) hm->message.p, hm->message.len); cs_sha1_update(&ctx, (const unsigned char *) m_sessions, sizeof(m_sessions)); std::string sys; sys = StdMetrics.ms_m_serial->AsString(); cs_sha1_update(&ctx, (const unsigned char *) sys.data(), sys.size()); sys = StdMetrics.ms_m_monotonic->AsString(); cs_sha1_update(&ctx, (const unsigned char *) sys.data(), sys.size()); sys = StdMetrics.ms_m_freeram->AsString(); cs_sha1_update(&ctx, (const unsigned char *) sys.data(), sys.size()); sys = StdMetrics.ms_v_bat_12v_voltage->AsString(); cs_sha1_update(&ctx, (const unsigned char *) sys.data(), sys.size()); union { uint64_t u64; unsigned char digest[20]; } buf; cs_sha1_final(buf.digest, &ctx); s->id = buf.u64; return s; } /** * DestroySession: delete (invalidate) a session */ void OvmsWebServer::DestroySession(user_session *s) { memset(s, 0, sizeof(*s)); } /** * CheckSessions: clean up sessions that have been idle for too long */ void OvmsWebServer::CheckSessions(void) { time_t threshold = mg_time() - SESSION_TTL; for (int i = 0; i < NUM_SESSIONS; i++) { user_session *s = &m_sessions[i]; if (s->id != 0 && s->last_used < threshold) { ESP_LOGD(TAG, "CheckSessions: session %" INT64_X_FMT " closed due to idleness.", s->id); DestroySession(s); } } } /** * HandleLogin: show/process login form * * This handler serves all URIs protected by PageAuth_Cookie, * redirects to original URI or /home as indicated. */ void OvmsWebServer::HandleLogin(PageEntry_t& p, PageContext_t& c) { std::string error; std::string uri = c.getvar("uri"); if (c.method == "POST") { // validate login: std::string username = c.getvar("username"); if (username == "") username = "admin"; std::string password = c.getvar("password"); user_session *s = NULL; if (!CheckLogin(username, password)) { error += "
  • Login validation failed, please check username & password
  • "; ESP_LOGW(TAG, "HandleLogin: auth failure for username '%s'", username.c_str()); } else if ((s = MyWebServer.CreateSession(c.hm)) == NULL) { error += "
  • Session creation failed, please try again later
  • "; } if (error == "") { // ok: set cookie, reload menu, redirect to /cfg/init, /cfg/password, original uri or /home: std::string init_step = MyConfig.GetParamValue("module", "init"); if (uri != "") c.uri = uri; else if (!init_step.empty() && init_step != "done") c.uri = "/cfg/init"; else if (MyConfig.GetParamValue("password", "module").empty()) c.uri = "/cfg/password"; else if (c.uri == "/login" || c.uri == "/logout" || c.uri == "/") c.uri = "/home"; char shead[200]; snprintf(shead, sizeof(shead), "Content-Type: text/html; charset=utf-8\r\n" "Cache-Control: no-cache\r\n" "Set-Cookie: %s=%" INT64_X_FMT "; path=/" , SESSION_COOKIE_NAME, s->id); c.head(200, shead); c.printf( "" , c.uri.c_str()); c.done(); ESP_LOGI(TAG, "HandleLogin: '%s' logged in, sid %" INT64_X_FMT, username.c_str(), s->id); return; } // output error, return to form: error = "

    Error!

      " + error + "
    "; c.head(403); c.alert("danger", error.c_str()); } else { if (c.uri != "/login") { c.head(403); c.alert("danger", "

    Login required

    "); } else { c.head(200); } } // generate form: c.panel_start("primary", "Login"); c.form_start(c.uri.c_str()); if (uri != "") c.printf("", c.encode_html(uri).c_str()); c.input_text("Username", "username", "", "empty = 'admin'", NULL, "autocomplete=\"section-login username\""); c.input_password("Password", "password", "", NULL, NULL, "autocomplete=\"section-login current-password\""); c.input_button("default", "Login"); c.form_end(); c.print(""); c.panel_end(); c.done(); } /** * HandleLogout: remove cookie and associated session state */ void OvmsWebServer::HandleLogout(PageEntry_t& p, PageContext_t& c) { user_session *s = MyWebServer.GetSession(c.hm); if (s != NULL) { ESP_LOGI(TAG, "HandleLogout: session %" INT64_X_FMT " destroyed", s->id); MyWebServer.DestroySession(s); } // erase cookie, reload menu & redirect to /home: char shead[200]; snprintf(shead, sizeof(shead), "Content-Type: text/html; charset=utf-8\r\n" "Cache-Control: no-cache\r\n" "Set-Cookie: %s=" , SESSION_COOKIE_NAME); c.head(200, shead); c.print( ""); c.done(); }