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
136 lines
5.0 KiB
C++
136 lines
5.0 KiB
C++
#include "share_rules.h"
|
|
|
|
#include <cstdio>
|
|
|
|
namespace roro::files {
|
|
|
|
namespace {
|
|
int hexDigit(char c) {
|
|
if (c >= '0' && c <= '9') return c - '0';
|
|
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
|
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
|
return -1;
|
|
}
|
|
// Whatever the two strings hold, the time taken says nothing about where they differ.
|
|
bool sameText(const std::string& a, const std::string& b) {
|
|
unsigned diff = static_cast<unsigned>(a.size() ^ b.size());
|
|
for (size_t i = 0; i < a.size() && i < b.size(); i++) diff |= static_cast<unsigned char>(a[i]) ^ static_cast<unsigned char>(b[i]);
|
|
return diff == 0;
|
|
}
|
|
} // namespace
|
|
|
|
std::string urlDecode(const std::string& text) {
|
|
std::string out;
|
|
out.reserve(text.size());
|
|
for (size_t i = 0; i < text.size(); i++) {
|
|
int hi, lo;
|
|
if (text[i] == '%' && i + 2 < text.size() + 0 && (hi = hexDigit(text[i + 1])) >= 0 && (lo = hexDigit(text[i + 2])) >= 0) {
|
|
out += static_cast<char>(hi * 16 + lo);
|
|
i += 2;
|
|
} else {
|
|
out += text[i];
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
bool queryParam(const std::string& query, const std::string& key, std::string& out) {
|
|
for (size_t at = 0; at <= query.size();) {
|
|
size_t amp = query.find('&', at);
|
|
if (amp == std::string::npos) amp = query.size();
|
|
size_t eq = query.find('=', at);
|
|
if (eq != std::string::npos && eq < amp && query.compare(at, eq - at, key) == 0) {
|
|
out = urlDecode(query.substr(eq + 1, amp - eq - 1));
|
|
return true;
|
|
}
|
|
at = amp + 1;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
std::string cookieValue(const std::string& header, const std::string& name) {
|
|
for (size_t at = 0; at < header.size();) {
|
|
while (at < header.size() && (header[at] == ' ' || header[at] == ';')) at++;
|
|
size_t end = header.find(';', at);
|
|
if (end == std::string::npos) end = header.size();
|
|
size_t eq = header.find('=', at);
|
|
if (eq != std::string::npos && eq < end && header.compare(at, eq - at, name) == 0) return header.substr(eq + 1, end - eq - 1);
|
|
at = end;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
std::string checkSharePath(const std::string& path) {
|
|
if (path.empty() || path[0] != '/') return "a path starts with /";
|
|
if (path.size() > 255) return "that path is too long";
|
|
if (path.size() > 1 && path.back() == '/') return "a path doesn't end with /";
|
|
for (size_t at = 1; at < path.size();) {
|
|
size_t end = path.find('/', at);
|
|
if (end == std::string::npos) end = path.size();
|
|
std::string part = path.substr(at, end - at);
|
|
if (part.empty() || part == "." || part == "..") return "that isn't a path on the card";
|
|
for (char c : part)
|
|
if (static_cast<unsigned char>(c) < 0x20 || c == 0x7F || c == '\\' || c == ':' || c == '*' || c == '?' || c == '"' || c == '<' || c == '>' || c == '|')
|
|
return "a name can't hold that character";
|
|
at = end + 1;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
std::string jsonString(const std::string& text) {
|
|
std::string out = "\"";
|
|
for (char c : text) {
|
|
unsigned char u = static_cast<unsigned char>(c);
|
|
if (c == '"' || c == '\\') {
|
|
out += '\\';
|
|
out += c;
|
|
} else if (u < 0x20) {
|
|
char buf[8];
|
|
std::snprintf(buf, sizeof buf, "\\u%04x", u);
|
|
out += buf;
|
|
} else {
|
|
out += c;
|
|
}
|
|
}
|
|
return out + "\"";
|
|
}
|
|
|
|
ShareListing::ShareListing(const std::string& path) : out_("{\"path\":" + jsonString(path) + ",\"items\":[") {}
|
|
|
|
void ShareListing::add(const std::string& name, uint32_t size, bool folder, int64_t modified) {
|
|
if (count_++) out_ += ',';
|
|
out_ += "{\"n\":" + jsonString(name) + ",\"s\":" + std::to_string(size) + ",\"d\":" + (folder ? "1" : "0") + ",\"t\":" + std::to_string(modified) + "}";
|
|
}
|
|
|
|
std::string ShareListing::json(bool more) { return out_ + "],\"more\":" + (more ? "true" : "false") + "}"; }
|
|
|
|
void ShareAuth::begin(const uint8_t random[4]) {
|
|
uint32_t n = (static_cast<uint32_t>(random[0]) << 24 | random[1] << 16 | random[2] << 8 | random[3]) % 1000000u;
|
|
char buf[8];
|
|
std::snprintf(buf, sizeof buf, "%06u", static_cast<unsigned>(n));
|
|
code_ = buf;
|
|
token_.clear();
|
|
gate_ = debug::AuthGate();
|
|
}
|
|
|
|
ShareAuth::Result ShareAuth::login(const std::string& code, uint32_t nowMs, const uint8_t random[16], std::string& token) {
|
|
if (code_.empty() || gate_.locked(nowMs)) return Result::Locked;
|
|
std::string digits;
|
|
for (char c : code)
|
|
if (c >= '0' && c <= '9') digits += c; // "123 456" is as good
|
|
if (!sameText(digits, code_)) return gate_.failed(nowMs) ? Result::Locked : Result::Wrong;
|
|
gate_.succeeded();
|
|
static const char* const kHex = "0123456789abcdef";
|
|
token_.clear();
|
|
for (int i = 0; i < 16; i++) {
|
|
token_ += kHex[random[i] >> 4];
|
|
token_ += kHex[random[i] & 15];
|
|
}
|
|
token = token_;
|
|
return Result::Ok;
|
|
}
|
|
|
|
bool ShareAuth::allowed(const std::string& token) const { return !token_.empty() && sameText(token, token_); }
|
|
|
|
} // namespace roro::files
|