Files
MeshCore/test/test_lightweight_ota_auth.py
T

126 lines
5.8 KiB
Python

"""Run the production lightweight HTTP parser/upload gate with in-memory sockets."""
from pathlib import Path
import unittest
from test_replay_reset_integration import extract_braced
import test_wifi_ota_start as wifi_start
ROOT = Path(__file__).resolve().parents[1]
FIXTURE = r'''
#define MESH_MANAGEMENT_PASSWORD_HOST_TEST 1
#include <helpers/esp32/ManagementPassword.h>
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cstdarg>
#include <string>
#include <strings.h>
uint32_t clock_ms = 100;
uint32_t millis() { return clock_ms; }
void delay(unsigned time) { clock_ms += time; }
int reboots = 0;
void esp_restart() { ++reboots; }
constexpr int U_FLASH = 0;
struct WiFiClient {
std::string input, output;
size_t position = 0;
bool open = true;
explicit WiFiClient(const std::string& wire) : input(wire) {}
bool connected() const { return open; }
int available() const { return input.size() - position; }
int read() { return available() ? uint8_t(input[position++]) : -1; }
int read(uint8_t* out, size_t count) {
count = count < size_t(available()) ? count : available();
memcpy(out, input.data() + position, count); position += count; return count;
}
void print(const char* value) { output += value; }
void printf(const char* format, ...) {
char text[512]; va_list args; va_start(args, format);
int count = vsnprintf(text, sizeof(text), format, args); va_end(args);
assert(count >= 0 && size_t(count) < sizeof(text)); output.append(text, count);
}
size_t write(const uint8_t* value, size_t count) { output.append(reinterpret_cast<const char*>(value), count); return count; }
void stop() { open = false; }
};
struct ESP32Board { void setInhibitSleep(bool) {} } board;
struct UpdateFake {
int begins = 0, writes = 0, ends = 0, aborts = 0;
bool begin(size_t, int) { ++begins; return true; }
size_t write(uint8_t*, size_t count) { ++writes; return count; }
void abort() { ++aborts; }
bool end() { ++ends; return true; }
const char* errorString() { return "fixture"; }
} Update;
class LightweightOTAServer {
public:
bool running = true;
ESP32Board* board = &::board;
int pages = 0, logs = 0;
void sendPage(WiFiClient& client) { ++pages; client.print("HTTP/1.1 200 OK"); }
void sendLog(WiFiClient& client) { ++logs; client.print("HTTP/1.1 200 OK"); }
@METHODS@
};
int main() {
using namespace mesh::esp32::managementPassword;
LightweightOTAServer server;
auto request = [&](const char* method, const char* path, const std::string& headers, const std::string& body = "") {
WiFiClient client(std::string(method) + " " + path + " HTTP/1.1\r\n" + headers + "\r\n" + body);
server.handleClient(client); return client.output;
};
assert(request("GET", "/update", "").find("503 Service Unavailable") != std::string::npos);
assert(publish("password"));
const std::string good = "Authorization: Basic YWRtaW46cGFzc3dvcmQ=\r\n";
for (const char* path : {"/", "/update", "/log"}) {
const auto absent = request("GET", path, "");
assert(absent.find("401 Unauthorized") != std::string::npos
&& absent.find("WWW-Authenticate: Basic realm=\"MeshCore (username: admin)\", charset=\"UTF-8\"") != std::string::npos);
const std::string help = "Sign in with username admin and this node's admin password.";
assert(absent.find("Content-Length: " + std::to_string(help.size()) + "\r\n") != std::string::npos);
assert(absent.substr(absent.find("\r\n\r\n") + 4) == help);
assert(request("GET", path, "Authorization: Basic wrong\r\n").find("401 Unauthorized") != std::string::npos);
assert(request("GET", path, good).find("200 OK") != std::string::npos);
}
assert(server.pages == 2 && server.logs == 1 && Update.begins == 0);
assert(request("GET", "/update", "Authorization:\r\n" + good).find("401 Unauthorized") != std::string::npos);
assert(request("GET", "/update", std::string("Authorization: ") + std::string(100, 'x') + "\r\n").find("401 Unauthorized") != std::string::npos);
const std::string payload(8, '\0');
assert(request("POST", "/update", "Content-Length: 8\r\n", payload).find("401 Unauthorized") != std::string::npos);
assert(Update.begins == 0 && Update.writes == 0 && reboots == 0);
assert(publish("different"));
assert(request("GET", "/log", good).find("401 Unauthorized") != std::string::npos);
const std::string updated = "Authorization: Basic YWRtaW46ZGlmZmVyZW50\r\n";
assert(request("GET", "/log", updated).find("200 OK") != std::string::npos);
{
ChangeGuard changing; assert(changing.acquired());
assert(request("POST", "/update", updated + "Content-Length: 8\r\n", payload).find("409 Conflict") != std::string::npos);
assert(!uploadActive() && Update.begins == 0);
}
assert(request("POST", "/update", updated + "Content-Length: 8\r\n", payload).find("200 OK") != std::string::npos);
assert(Update.begins == 1 && Update.writes == 1 && Update.ends == 1 && reboots == 1 && !uploadActive());
}
'''
class LightweightOtaAuthTest(unittest.TestCase):
compile_and_run = wifi_start.WiFiOtaStartTest.compile_and_run
def test_http_routes_and_upload_admission_use_committed_admin(self):
source = (ROOT / "src/helpers/ESP32Board.cpp").read_text()
methods = "\n".join(extract_braced(source, signature) for signature in (
" static void sendResponse(", " bool readLine(", " void sendUpdateError(",
" void receiveUpdate(", " void handleClient("))
fixture = FIXTURE.replace("@METHODS@", methods)
self.compile_and_run(fixture)
gate = " if (auth != mesh::esp32::managementPassword::AuthResult::Authorized) {"
old = fixture.replace(gate, " if (false) {")
self.assertNotEqual(old, fixture)
with self.assertRaises(AssertionError):
self.compile_and_run(old)
if __name__ == "__main__":
unittest.main()