mirror of
https://github.com/esphome/esphome.git
synced 2026-08-17 02:47:52 +08:00
[web_server] Fix basic auth with long credentials (#18237)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import gzip
|
||||
import logging
|
||||
import re
|
||||
@@ -406,10 +407,21 @@ async def to_code(config):
|
||||
# The scheme is fixed at build time so the unused Basic/Digest code path is compiled
|
||||
# out. Basic is the current default (the absence of this define); an explicit
|
||||
# 'type: digest' opts in early. Default changes to digest in 2027.1.0.
|
||||
if auth.get(CONF_TYPE) == AUTH_TYPE_DIGEST:
|
||||
is_digest = auth.get(CONF_TYPE) == AUTH_TYPE_DIGEST
|
||||
if is_digest:
|
||||
cg.add_define("USE_WEBSERVER_AUTH_DIGEST")
|
||||
cg.add(paren.set_auth_username(auth[CONF_USERNAME]))
|
||||
cg.add(paren.set_auth_password(auth[CONF_PASSWORD]))
|
||||
if is_digest or CORE.is_esp32:
|
||||
cg.add(paren.set_auth_username(auth[CONF_USERNAME]))
|
||||
cg.add(paren.set_auth_password(auth[CONF_PASSWORD]))
|
||||
else:
|
||||
# Every non-ESP32 basic auth build takes this path. The ESP8266 and RP2040
|
||||
# core base64 encoders wrap output every 72 chars, which breaks
|
||||
# ESPAsyncWebServer's basic auth compare for long credentials.
|
||||
# Precompute the hash here and let C++ compare the raw header payload.
|
||||
basic_hash = base64.b64encode(
|
||||
f"{auth[CONF_USERNAME]}:{auth[CONF_PASSWORD]}".encode()
|
||||
).decode()
|
||||
cg.add(paren.set_auth_basic_hash(basic_hash))
|
||||
if CONF_CSS_INCLUDE in config:
|
||||
cg.add_define("USE_WEBSERVER_CSS_INCLUDE")
|
||||
path = CORE.relative_config_path(config[CONF_CSS_INCLUDE])
|
||||
|
||||
@@ -7,7 +7,7 @@ WebServerBase *global_web_server_base = nullptr; // NOLINT(cppcoreguidelines-av
|
||||
|
||||
void WebServerBase::add_handler(AsyncWebHandler *handler) {
|
||||
#ifdef USE_WEBSERVER_AUTH
|
||||
if (!credentials_.username.empty()) {
|
||||
if (credentials_.is_set()) {
|
||||
handler = new internal::AuthMiddlewareHandler(handler, &credentials_);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
#include "esphome/core/defines.h"
|
||||
#if defined(USE_NETWORK) && !defined(USE_ZEPHYR)
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/core/progmem.h"
|
||||
@@ -46,9 +45,20 @@ class MiddlewareHandler : public AsyncWebHandler {
|
||||
};
|
||||
|
||||
#ifdef USE_WEBSERVER_AUTH
|
||||
// All fields point to string literals in generated code; nothing is copied.
|
||||
struct Credentials {
|
||||
std::string username;
|
||||
std::string password;
|
||||
#if USE_ESP32 || defined(USE_WEBSERVER_AUTH_DIGEST)
|
||||
const char *username{nullptr};
|
||||
const char *password{nullptr};
|
||||
bool is_set() const { return username != nullptr; }
|
||||
#else
|
||||
// base64("username:password"), precomputed at codegen time. Used by every non-ESP32 basic
|
||||
// auth build. The ESP8266 and RP2040 core libb64 wraps base64 output every 72 chars, so
|
||||
// letting the library encode and compare fails for long credentials; instead the header
|
||||
// payload is compared against this hash.
|
||||
const char *basic_auth_hash{nullptr};
|
||||
bool is_set() const { return basic_auth_hash != nullptr; }
|
||||
#endif
|
||||
};
|
||||
|
||||
class AuthMiddlewareHandler : public MiddlewareHandler {
|
||||
@@ -57,10 +67,14 @@ class AuthMiddlewareHandler : public MiddlewareHandler {
|
||||
: MiddlewareHandler(next), credentials_(credentials) {}
|
||||
|
||||
bool check_auth(AsyncWebServerRequest *request) {
|
||||
bool success = request->authenticate(credentials_->username.c_str(), credentials_->password.c_str());
|
||||
// The scheme is chosen at build time (USE_WEBSERVER_AUTH_DIGEST); the unused path is
|
||||
// compiled out. On ESP32 our own server picks the scheme internally.
|
||||
#if USE_ESP32 || defined(USE_WEBSERVER_AUTH_DIGEST)
|
||||
bool success = request->authenticate(credentials_->username, credentials_->password);
|
||||
#else
|
||||
bool success = request->authenticate(credentials_->basic_auth_hash);
|
||||
#endif
|
||||
if (!success) {
|
||||
// The scheme is chosen at build time (USE_WEBSERVER_AUTH_DIGEST); the unused path is
|
||||
// compiled out. On ESP32 our own server picks the scheme internally.
|
||||
#if USE_ESP32
|
||||
request->requestAuthentication();
|
||||
#elif defined(USE_WEBSERVER_AUTH_DIGEST)
|
||||
@@ -125,8 +139,12 @@ class WebServerBase final {
|
||||
AsyncWebServer *get_server() const { return this->server_; }
|
||||
|
||||
#ifdef USE_WEBSERVER_AUTH
|
||||
void set_auth_username(std::string auth_username) { credentials_.username = std::move(auth_username); }
|
||||
void set_auth_password(std::string auth_password) { credentials_.password = std::move(auth_password); }
|
||||
#if USE_ESP32 || defined(USE_WEBSERVER_AUTH_DIGEST)
|
||||
void set_auth_username(const char *auth_username) { credentials_.username = auth_username; }
|
||||
void set_auth_password(const char *auth_password) { credentials_.password = auth_password; }
|
||||
#else
|
||||
void set_auth_basic_hash(const char *hash) { credentials_.basic_auth_hash = hash; }
|
||||
#endif
|
||||
#endif
|
||||
|
||||
void add_handler(AsyncWebHandler *handler);
|
||||
|
||||
@@ -33,14 +33,49 @@ def test_web_server_auth_explicit_basic_no_warning(
|
||||
generate_main: Callable[[str], str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Auth type basic builds Basic and does not warn."""
|
||||
generate_main("tests/component_tests/web_server/web_server_auth_basic.yaml")
|
||||
"""Auth type basic on ESP32 uses plaintext credentials and does not warn."""
|
||||
main_cpp = generate_main(
|
||||
"tests/component_tests/web_server/web_server_auth_basic.yaml"
|
||||
)
|
||||
|
||||
assert '->set_auth_username("admin");' in main_cpp
|
||||
assert '->set_auth_password("password");' in main_cpp
|
||||
assert "set_auth_basic_hash" not in main_cpp
|
||||
assert _has_define("USE_WEBSERVER_AUTH")
|
||||
assert not _has_define("USE_WEBSERVER_AUTH_DIGEST")
|
||||
assert _DEFAULT_CHANGE_WARNING not in caplog.text
|
||||
|
||||
|
||||
def test_web_server_auth_basic_esp8266_uses_precomputed_hash(
|
||||
generate_main: Callable[[str], str],
|
||||
) -> None:
|
||||
"""Auth type basic on ESP8266 emits the precomputed base64 hash, not the credentials."""
|
||||
main_cpp = generate_main(
|
||||
"tests/component_tests/web_server/web_server_auth_basic_esp8266.yaml"
|
||||
)
|
||||
|
||||
assert '->set_auth_basic_hash("YWRtaW46cGFzc3dvcmQ=");' in main_cpp
|
||||
assert "set_auth_username" not in main_cpp
|
||||
assert "set_auth_password" not in main_cpp
|
||||
assert _has_define("USE_WEBSERVER_AUTH")
|
||||
assert not _has_define("USE_WEBSERVER_AUTH_DIGEST")
|
||||
|
||||
|
||||
def test_web_server_auth_digest_esp8266_uses_plaintext_credentials(
|
||||
generate_main: Callable[[str], str],
|
||||
) -> None:
|
||||
"""Auth type digest on ESP8266 uses plaintext credentials, not the basic hash."""
|
||||
main_cpp = generate_main(
|
||||
"tests/component_tests/web_server/web_server_auth_digest_esp8266.yaml"
|
||||
)
|
||||
|
||||
assert '->set_auth_username("admin");' in main_cpp
|
||||
assert '->set_auth_password("password");' in main_cpp
|
||||
assert "set_auth_basic_hash" not in main_cpp
|
||||
assert _has_define("USE_WEBSERVER_AUTH")
|
||||
assert _has_define("USE_WEBSERVER_AUTH_DIGEST")
|
||||
|
||||
|
||||
def test_web_server_auth_explicit_digest(
|
||||
generate_main: Callable[[str], str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp8266:
|
||||
board: esp01_1m
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
web_server:
|
||||
auth:
|
||||
username: admin
|
||||
password: password
|
||||
type: basic
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp8266:
|
||||
board: esp01_1m
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
web_server:
|
||||
auth:
|
||||
username: admin
|
||||
password: password
|
||||
type: digest
|
||||
@@ -0,0 +1,8 @@
|
||||
packages:
|
||||
web_server: !include common_v2.yaml
|
||||
|
||||
web_server:
|
||||
auth:
|
||||
username: admin
|
||||
password: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
type: basic
|
||||
@@ -4,5 +4,5 @@ packages:
|
||||
web_server:
|
||||
auth:
|
||||
username: admin
|
||||
password: password
|
||||
password: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
type: basic
|
||||
|
||||
Reference in New Issue
Block a user