Public Access
`w` in the Storage App starts a small HTTP server and shows its address, as a QR code and in letters, with a six-digit code. A browser on the same network that has typed the code can list, download, upload, make folders and delete, under the Storage App's rules. The server runs only while that screen is open. Nothing is encrypted, and the screen says so. Uploads are streamed to the card under a temporary name and renamed when whole. Every access to the card is handed to the storage task, 8 KB at a time, from the server's own task. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
102 lines
4.9 KiB
C++
102 lines
4.9 KiB
C++
#include <unity.h>
|
|
|
|
#include <string>
|
|
|
|
#include "share_rules.h"
|
|
|
|
using namespace roro::files;
|
|
|
|
void setUp() {}
|
|
void tearDown() {}
|
|
|
|
void test_what_a_request_asks_for() {
|
|
TEST_ASSERT_EQUAL_STRING("/notes/my list.txt", urlDecode("%2Fnotes%2Fmy%20list.txt").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("a+b", urlDecode("a+b").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("100%", urlDecode("100%").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("%zz", urlDecode("%zz").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("\xC3\xA9t\xC3\xA9", urlDecode("%C3%A9t%C3%a9").c_str());
|
|
std::string v;
|
|
TEST_ASSERT_TRUE(queryParam("path=%2Fnotes&replace=1", "path", v));
|
|
TEST_ASSERT_EQUAL_STRING("/notes", v.c_str());
|
|
TEST_ASSERT_TRUE(queryParam("path=%2Fnotes&replace=1", "replace", v));
|
|
TEST_ASSERT_EQUAL_STRING("1", v.c_str());
|
|
TEST_ASSERT_FALSE(queryParam("xpath=1&pathx=2", "path", v));
|
|
TEST_ASSERT_FALSE(queryParam("", "path", v));
|
|
TEST_ASSERT_TRUE(queryParam("a=&path=", "path", v));
|
|
TEST_ASSERT_EQUAL_STRING("", v.c_str());
|
|
TEST_ASSERT_EQUAL_STRING("abc123", cookieValue("theme=dark; s=abc123; x=1", "s").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("abc123", cookieValue("s=abc123", "s").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("", cookieValue("ss=abc123; xs=1", "s").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("", cookieValue("", "s").c_str());
|
|
}
|
|
|
|
void test_paths_a_browser_may_name() {
|
|
TEST_ASSERT_EQUAL_STRING("", checkSharePath("/").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("", checkSharePath("/notes/my list (2).txt").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("", checkSharePath("/gemini/saved/\xC3\xA9t\xC3\xA9.gmi").c_str());
|
|
for (const char* bad : {"", "notes", "/notes/", "/notes/../wifi", "/..", "/a//b", "/a/./b", "/a\\b", "/a/b\n", "/a:b", "/what?", "/a*"})
|
|
TEST_ASSERT_TRUE_MESSAGE(!checkSharePath(bad).empty(), bad);
|
|
TEST_ASSERT_TRUE(!checkSharePath("/" + std::string(300, 'a')).empty());
|
|
}
|
|
|
|
void test_json() {
|
|
TEST_ASSERT_EQUAL_STRING("\"plain\"", jsonString("plain").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("\"a \\\"b\\\" \\\\ c\"", jsonString("a \"b\" \\ c").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("\"tab\\u0009\"", jsonString("tab\t").c_str());
|
|
TEST_ASSERT_EQUAL_STRING("\"\xC3\xA9\"", jsonString("\xC3\xA9").c_str());
|
|
ShareListing empty("/");
|
|
TEST_ASSERT_EQUAL_STRING("{\"path\":\"/\",\"items\":[],\"more\":false}", empty.json(false).c_str());
|
|
ShareListing l("/notes");
|
|
l.add("a \"b\".txt", 12, false, 1791400000);
|
|
l.add("old", 0, true, 0);
|
|
TEST_ASSERT_EQUAL_size_t(2, l.count());
|
|
TEST_ASSERT_EQUAL_STRING("{\"path\":\"/notes\",\"items\":[{\"n\":\"a \\\"b\\\".txt\",\"s\":12,\"d\":0,\"t\":1791400000},{\"n\":\"old\",\"s\":0,\"d\":1,\"t\":0}],\"more\":true}",
|
|
l.json(true).c_str());
|
|
}
|
|
|
|
void test_the_code_and_the_token() {
|
|
ShareAuth auth;
|
|
std::string token;
|
|
const uint8_t r16[16] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff};
|
|
TEST_ASSERT_TRUE(auth.login("000000", 0, r16, token) == ShareAuth::Result::Locked); // not started: nobody gets in
|
|
TEST_ASSERT_FALSE(auth.allowed(""));
|
|
const uint8_t r4[4] = {0x00, 0x00, 0x30, 0x39}; // 12345
|
|
auth.begin(r4);
|
|
TEST_ASSERT_EQUAL_STRING("012345", auth.code().c_str());
|
|
TEST_ASSERT_FALSE(auth.allowed(""));
|
|
TEST_ASSERT_TRUE(auth.login("12345", 1000, r16, token) == ShareAuth::Result::Wrong); // the leading zero counts
|
|
TEST_ASSERT_TRUE(token.empty());
|
|
TEST_ASSERT_TRUE(auth.login("012 345", 2000, r16, token) == ShareAuth::Result::Ok); // typed as the screen groups it
|
|
TEST_ASSERT_EQUAL_STRING("00112233445566778899aabbccddeeff", token.c_str());
|
|
TEST_ASSERT_TRUE(auth.allowed(token));
|
|
TEST_ASSERT_FALSE(auth.allowed(token + "0"));
|
|
TEST_ASSERT_FALSE(auth.allowed("00112233445566778899aabbccddeef0"));
|
|
// Sharing started again: a new code, and yesterday's browser is out.
|
|
const uint8_t again[4] = {0xFF, 0xFF, 0xFF, 0xFF};
|
|
auth.begin(again);
|
|
TEST_ASSERT_EQUAL_size_t(6, auth.code().size());
|
|
TEST_ASSERT_FALSE(auth.allowed(token));
|
|
}
|
|
|
|
void test_five_wrong_codes_close_it_for_a_minute() {
|
|
ShareAuth auth;
|
|
const uint8_t r4[4] = {0, 0, 0, 7};
|
|
const uint8_t r16[16] = {0};
|
|
auth.begin(r4);
|
|
std::string token;
|
|
for (int i = 0; i < 4; i++) TEST_ASSERT_TRUE(auth.login("999999", 1000, r16, token) == ShareAuth::Result::Wrong);
|
|
TEST_ASSERT_TRUE(auth.login("999999", 1000, r16, token) == ShareAuth::Result::Locked);
|
|
TEST_ASSERT_TRUE(auth.login("000007", 30000, r16, token) == ShareAuth::Result::Locked); // the right one too
|
|
TEST_ASSERT_TRUE(auth.login("000007", 62000, r16, token) == ShareAuth::Result::Ok);
|
|
}
|
|
|
|
int main() {
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_what_a_request_asks_for);
|
|
RUN_TEST(test_paths_a_browser_may_name);
|
|
RUN_TEST(test_json);
|
|
RUN_TEST(test_the_code_and_the_token);
|
|
RUN_TEST(test_five_wrong_codes_close_it_for_a_minute);
|
|
return UNITY_END();
|
|
}
|