|
|
|
@@ -66,9 +66,12 @@ static const char *const TAG = "web_server";
|
|
|
|
|
// GET /{domain}/{device_name}/{entity_name} - sub-device state (USE_DEVICES only)
|
|
|
|
|
// POST /{domain}/{device_name}/{entity_name}/{action} - sub-device action (USE_DEVICES only)
|
|
|
|
|
static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain, bool is_post = false) {
|
|
|
|
|
// Every path returns this one object so it is built in place; fields are only set once the URL is known valid
|
|
|
|
|
UrlMatch match{};
|
|
|
|
|
|
|
|
|
|
// URL must start with '/' and have content after it
|
|
|
|
|
if (url_len < 2 || url_ptr[0] != '/')
|
|
|
|
|
return UrlMatch{};
|
|
|
|
|
return match;
|
|
|
|
|
|
|
|
|
|
const char *p = url_ptr + 1;
|
|
|
|
|
const char *end = url_ptr + url_len;
|
|
|
|
@@ -90,15 +93,14 @@ static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain,
|
|
|
|
|
|
|
|
|
|
// Must have domain with trailing slash
|
|
|
|
|
if (!s2)
|
|
|
|
|
return UrlMatch{};
|
|
|
|
|
|
|
|
|
|
UrlMatch match{};
|
|
|
|
|
match.domain = make_ref(s1, s2);
|
|
|
|
|
match.valid = true;
|
|
|
|
|
|
|
|
|
|
if (only_domain || s2 >= end)
|
|
|
|
|
return match;
|
|
|
|
|
|
|
|
|
|
if (only_domain || s2 >= end) {
|
|
|
|
|
match.domain = make_ref(s1, s2);
|
|
|
|
|
match.valid = true;
|
|
|
|
|
return match;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse remaining segments only when needed
|
|
|
|
|
const char *s3 = next_segment(s2);
|
|
|
|
|
const char *s4 = s3 ? next_segment(s3) : nullptr;
|
|
|
|
@@ -109,7 +111,7 @@ static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain,
|
|
|
|
|
|
|
|
|
|
// Reject empty segments
|
|
|
|
|
if (seg2.empty() || (s3 && seg3.empty()) || (s4 && seg4.empty()))
|
|
|
|
|
return UrlMatch{};
|
|
|
|
|
return match;
|
|
|
|
|
|
|
|
|
|
// Interpret based on segment count
|
|
|
|
|
if (!s3) {
|
|
|
|
@@ -121,28 +123,31 @@ static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain,
|
|
|
|
|
if (is_post) {
|
|
|
|
|
match.id = seg2;
|
|
|
|
|
match.method = seg3;
|
|
|
|
|
return match;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
#ifdef USE_DEVICES
|
|
|
|
|
match.device_name = seg2;
|
|
|
|
|
match.id = seg3;
|
|
|
|
|
match.device_name = seg2;
|
|
|
|
|
match.id = seg3;
|
|
|
|
|
#else
|
|
|
|
|
return UrlMatch{}; // 3-segment GET not supported without USE_DEVICES
|
|
|
|
|
return match; // 3-segment GET not supported without USE_DEVICES
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 3 segments after domain: /{domain}/{device}/{entity}/{action}
|
|
|
|
|
#ifdef USE_DEVICES
|
|
|
|
|
if (!is_post) {
|
|
|
|
|
return UrlMatch{}; // 4-segment GET not supported (action requires POST)
|
|
|
|
|
return match; // 4-segment GET not supported (action requires POST)
|
|
|
|
|
}
|
|
|
|
|
match.device_name = seg2;
|
|
|
|
|
match.id = seg3;
|
|
|
|
|
match.method = seg4;
|
|
|
|
|
#else
|
|
|
|
|
return UrlMatch{}; // Not supported without USE_DEVICES
|
|
|
|
|
// Not supported without USE_DEVICES
|
|
|
|
|
return match;
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match.domain = make_ref(s1, s2);
|
|
|
|
|
match.valid = true;
|
|
|
|
|
return match;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -336,6 +341,9 @@ void DeferredUpdateEventSourceList::on_client_disconnect_(DeferredUpdateEventSou
|
|
|
|
|
|
|
|
|
|
WebServer::WebServer(web_server_base::WebServerBase *base) : base_(base) {}
|
|
|
|
|
|
|
|
|
|
// Kept out of the callers so the 64 bit division is emitted once
|
|
|
|
|
__attribute__((noinline)) static uint32_t uptime_seconds() { return static_cast<uint32_t>(millis_64() / 1000); }
|
|
|
|
|
|
|
|
|
|
json::SerializationBuffer<> WebServer::get_config_json() {
|
|
|
|
|
json::JsonBuilder builder;
|
|
|
|
|
JsonObject root = builder.root();
|
|
|
|
@@ -343,7 +351,7 @@ json::SerializationBuffer<> WebServer::get_config_json() {
|
|
|
|
|
root[ESPHOME_F("title")] = App.get_friendly_name().empty() ? App.get_name().c_str() : App.get_friendly_name().c_str();
|
|
|
|
|
char comment_buffer[Application::ESPHOME_COMMENT_SIZE_MAX];
|
|
|
|
|
App.get_comment_string(comment_buffer);
|
|
|
|
|
root[ESPHOME_F("comment")] = comment_buffer;
|
|
|
|
|
root[ESPHOME_F("comment")] = static_cast<const char *>(comment_buffer);
|
|
|
|
|
#if defined(USE_WEBSERVER_OTA_DISABLED) || !defined(USE_WEBSERVER_OTA)
|
|
|
|
|
root[ESPHOME_F("ota")] = false; // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal
|
|
|
|
|
#else
|
|
|
|
@@ -351,7 +359,7 @@ json::SerializationBuffer<> WebServer::get_config_json() {
|
|
|
|
|
#endif
|
|
|
|
|
root[ESPHOME_F("log")] = this->expose_log_;
|
|
|
|
|
root[ESPHOME_F("lang")] = "en";
|
|
|
|
|
root[ESPHOME_F("uptime")] = static_cast<uint32_t>(millis_64() / 1000);
|
|
|
|
|
root[ESPHOME_F("uptime")] = uptime_seconds();
|
|
|
|
|
|
|
|
|
|
return builder.serialize();
|
|
|
|
|
}
|
|
|
|
@@ -382,7 +390,7 @@ void WebServer::setup() {
|
|
|
|
|
if (this->events_.empty())
|
|
|
|
|
return;
|
|
|
|
|
char buf[32];
|
|
|
|
|
auto uptime = static_cast<uint32_t>(millis_64() / 1000);
|
|
|
|
|
auto uptime = uptime_seconds();
|
|
|
|
|
size_t len = buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%" PRIu32 "}", uptime);
|
|
|
|
|
this->events_.try_send_nodefer(buf, len, "ping", millis(), 30000);
|
|
|
|
|
});
|
|
|
|
@@ -467,7 +475,10 @@ bool WebServer::is_request_origin_allowed_(AsyncWebServerRequest *request, const
|
|
|
|
|
const size_t scheme_sep = origin.find("://");
|
|
|
|
|
if (scheme_sep != std::string::npos) {
|
|
|
|
|
const std::string host = get_request_header(request, "Host");
|
|
|
|
|
if (!host.empty() && origin.compare(scheme_sep + 3, std::string::npos, host) == 0)
|
|
|
|
|
// Compare by hand: compare(pos, ...) carries an out_of_range throw path that can never fire here
|
|
|
|
|
const size_t authority = scheme_sep + 3;
|
|
|
|
|
if (!host.empty() && origin.size() - authority == host.size() &&
|
|
|
|
|
memcmp(origin.data() + authority, host.data(), host.size()) == 0)
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -534,7 +545,7 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) {
|
|
|
|
|
// Helper functions to reduce code size by avoiding macro expansion
|
|
|
|
|
// Build unique id as: {domain}/{device_name}/{entity_name} or {domain}/{entity_name}
|
|
|
|
|
// Uses names (not object_id) to avoid UTF-8 collision issues
|
|
|
|
|
static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, JsonDetail start_config) {
|
|
|
|
|
static void set_json_id(JsonObject root, EntityBase *obj, const char *prefix, JsonDetail start_config) {
|
|
|
|
|
const StringRef &name = obj->get_name();
|
|
|
|
|
size_t prefix_len = strlen(prefix);
|
|
|
|
|
size_t name_len = name.size();
|
|
|
|
@@ -569,7 +580,7 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J
|
|
|
|
|
#endif
|
|
|
|
|
memcpy(p, name.c_str(), name_len);
|
|
|
|
|
p[name_len] = '\0';
|
|
|
|
|
root[ESPHOME_F("id")] = id_buf;
|
|
|
|
|
root[ESPHOME_F("id")] = static_cast<const char *>(id_buf);
|
|
|
|
|
|
|
|
|
|
if (start_config == DETAIL_ALL) {
|
|
|
|
|
root[ESPHOME_F("domain")] = prefix;
|
|
|
|
@@ -594,14 +605,13 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J
|
|
|
|
|
// Keep as separate function even though only used once: reduces code size by ~48 bytes
|
|
|
|
|
// by allowing compiler to share code between template instantiations (bool, float, etc.)
|
|
|
|
|
template<typename T>
|
|
|
|
|
static void set_json_value(JsonObject &root, EntityBase *obj, const char *prefix, const T &value,
|
|
|
|
|
JsonDetail start_config) {
|
|
|
|
|
static void set_json_value(JsonObject root, EntityBase *obj, const char *prefix, T value, JsonDetail start_config) {
|
|
|
|
|
set_json_id(root, obj, prefix, start_config);
|
|
|
|
|
root[ESPHOME_F("value")] = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template<typename S, typename T>
|
|
|
|
|
static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, S state, const T &value,
|
|
|
|
|
static void set_json_icon_state_value(JsonObject root, EntityBase *obj, const char *prefix, S state, T value,
|
|
|
|
|
JsonDetail start_config) {
|
|
|
|
|
set_json_value(root, obj, prefix, value, start_config);
|
|
|
|
|
root[ESPHOME_F("state")] = state;
|
|
|
|
@@ -1230,7 +1240,7 @@ json::SerializationBuffer<> WebServer::date_json_(datetime::DateEntity *obj, Jso
|
|
|
|
|
// Format: YYYY-MM-DD (max 10 chars + null)
|
|
|
|
|
char value[12];
|
|
|
|
|
buf_append_printf(value, sizeof(value), 0, "%d-%02d-%02d", obj->year, obj->month, obj->day);
|
|
|
|
|
set_json_icon_state_value(root, obj, "date", value, value, start_config);
|
|
|
|
|
set_json_icon_state_value<const char *, const char *>(root, obj, "date", value, value, start_config);
|
|
|
|
|
if (start_config == DETAIL_ALL) {
|
|
|
|
|
this->add_sorting_info_(root, obj);
|
|
|
|
|
}
|
|
|
|
@@ -1290,7 +1300,7 @@ json::SerializationBuffer<> WebServer::time_json_(datetime::TimeEntity *obj, Jso
|
|
|
|
|
// Format: HH:MM:SS (8 chars + null)
|
|
|
|
|
char value[12];
|
|
|
|
|
buf_append_printf(value, sizeof(value), 0, "%02d:%02d:%02d", obj->hour, obj->minute, obj->second);
|
|
|
|
|
set_json_icon_state_value(root, obj, "time", value, value, start_config);
|
|
|
|
|
set_json_icon_state_value<const char *, const char *>(root, obj, "time", value, value, start_config);
|
|
|
|
|
if (start_config == DETAIL_ALL) {
|
|
|
|
|
this->add_sorting_info_(root, obj);
|
|
|
|
|
}
|
|
|
|
@@ -1351,7 +1361,7 @@ json::SerializationBuffer<> WebServer::datetime_json_(datetime::DateTimeEntity *
|
|
|
|
|
char value[24];
|
|
|
|
|
buf_append_printf(value, sizeof(value), 0, "%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour,
|
|
|
|
|
obj->minute, obj->second);
|
|
|
|
|
set_json_icon_state_value(root, obj, "datetime", value, value, start_config);
|
|
|
|
|
set_json_icon_state_value<const char *, const char *>(root, obj, "datetime", value, value, start_config);
|
|
|
|
|
if (start_config == DETAIL_ALL) {
|
|
|
|
|
this->add_sorting_info_(root, obj);
|
|
|
|
|
}
|
|
|
|
@@ -2295,7 +2305,7 @@ json::SerializationBuffer<> WebServer::update_json_(update::UpdateEntity *obj, J
|
|
|
|
|
JsonObject root = builder.root();
|
|
|
|
|
|
|
|
|
|
set_json_icon_state_value(root, obj, "update", json_state_str(update::update_state_to_string(obj->state)),
|
|
|
|
|
obj->update_info.latest_version, start_config);
|
|
|
|
|
obj->update_info.latest_version.c_str(), start_config);
|
|
|
|
|
if (start_config == DETAIL_ALL) {
|
|
|
|
|
root[ESPHOME_F("current_version")] = obj->update_info.current_version;
|
|
|
|
|
root[ESPHOME_F("title")] = obj->update_info.title;
|
|
|
|
|