#define CPPHTTPLIB_OPENSSL_SUPPORT #include "httplib.h" #include "json.hpp" #include #include #include #include #include #include #include #include #include #include #include #include using json = nlohmann::json; namespace fs = std::filesystem; static const char* WORDS[] = { "acid","acorn","acre","acts","afar","affix","aged","agent","agile","aging", "agony","ahead","aide","ajar","alarm","alias","alibi","alien","align","alike", "alive","alley","allot","allow","alloy","aloft","alone","alpha","altar","alter", "amino","ample","amuse","angel","anger","angle","angry","ankle","annex","anvil", "apart","apex","arena","argue","arise","armor","army","aroma","array","arrow", "arson","art","ash","aside","atlas","atom","attic","audio","audit","augur", "avid","avoid","awake","award","aware","axiom","axis","bacon","badge","badly", "bagel","baker","balmy","ban","band","banjo","barge","baron","base","basic", "basin","batch","bath","baton","beach","beast","beer","begin","being","belly", "belt","bench","berry","bird","birth","black","blade","blame","blank","blast", "blaze","bleak","blend","bless","blimp","blind","bliss","blitz","block","blood", "bloom","blown","blues","bluff","blunt","board","boast","body","boil","bold", "bolt","bond","bone","bonus","book","boost","born","boss","botch","both", "boxer","brace","brain","brand","brave","bread","break","breed","brick","brief", "bring","brink","brisk","broad","broil","broke","brook","broth","brown","brush", "buddy","buggy","build","built","bulge","bulk","bully","bunch","bunny","burn", "burst","buyer","cabin","cable","camel","camp","candy","canon","cape","card", "cargo","carol","carry","carve","case","cash","cause","cedar","chain","chair", "chalk","champ","chant","chaos","charm","chase","cheap","check","cheek","cheer", "chess","chest","chief","child","chill","chip","chord","chore","chunk","churn", "cider","cigar","cinch","cite","civic","civil","claim","clamp","clap","clash", "clasp","class","claw","clay","clean","clear","clerk","click","cliff","climb", "cling","clip","cloak","clock","clone","close","cloth","cloud","clown","club", "cluck","clue","clump","coach","coast","coat","cobra","cocoa","coil","coin", "coke","cold","comet","comic","comma","conch","coral","cord","core","cork", "corn","couch","count","coup","court","cover","craft","crane","crash","crate", "crawl","crazy","cream","crew","crime","crisp","cross","crowd","crown","crude", "crush","cube","curve","cycle","daily","dance","dare","dark","dart","dash", "data","dawn","dealt","death","debug","decay","decor","decoy","decry","deed", "delay","delta","delve","demon","depot","depth","derby","desk","detox","deuce", "devil","diary","dig","digit","dime","dine","disco","dish","disk","ditch", "diver","dizzy","dodge","doing","donor","doom","door","dose","dot","doubt", "dough","dove","down","draft","drain","drake","drama","drank","drape","draw", "dream","dress","dried","drift","drill","drink","drive","drone","drool","drop", "drove","drown","drum","drunk","dryer","duck","dug","dummy","dune","dusk", "dust","duty","dwarf","dwell","dying","eager","eagle","earth","easel","east", "eaten","eater","ebony","echo","eclipse","edge","eel","eight","elbow","elder", "elect","elite","elm","elves","ember","emit","empty","ended","enemy","enjoy", "enter","entry","envoy","equal","equip","erase","error","essay","ethic","evade", "even","event","every","evil","evoke","exact","exam","exert","exile","exist", "exit","exile","extra","fable","faced","facet","fact","faint","fairy","faith", "false","fame","fancy","fang","far","farce","farm","fatal","fault","feast", "feat","fence","ferry","fetch","fever","fiber","fifth","fifty","fight","film", "final","finch","find","fine","fire","firm","first","fish","fist","five", "fixed","flag","flame","flash","flask","flat","flaw","flesh","flick","flies", "fling","flint","flip","float","flock","flood","floor","flour","flow","fluid", "fluke","flung","flunk","flush","flute","foam","focal","focus","foggy","foil", "font","food","forge","form","fort","forum","fossil","found","fox","foyer", "frail","frame","frank","fraud","freak","fresh","fried","frill","frisk","front", "frost","froze","fruit","fuel","fully","fungi","fury","fuse","fussy","fuzzy", "gauge","gave","gavel","gaze","gear","geek","gem","gene","genie","genre", "ghost","giant","gift","given","glad","glass","glide","gloom","glory","gloss", "glove","glow","glue","going","gold","golf","gone","goose","gorge","gown", "grab","grace","grade","grain","grand","grant","grape","graph","grasp","grass", "grave","gravy","great","greed","green","greet","grief","grill","grin","grind", "grip","groan","groom","gross","group","grove","grow","grown","guard","guess", "guide","guild","guilt","guise","gulch","gulf","gummy","guru","gusty","habit", "half","halt","happy","hard","harm","harsh","haste","hatch","haunt","haven", "hazel","heart","heave","heavy","hedge","hefty","hello","hence","herb","herd", "hero","heron","hiker","hill","hinge","hippo","hire","hitch","hive","hobby", "hold","honey","honor","hood","hook","hoped","horn","horse","host","hotel", "hound","house","hover","human","humid","humor","hung","hunky","hunt","hurry", "icing","icon","ideal","idiom","idiot","idle","idly","image","imp","imply", "inbox","inch","index","inert","infer","inner","input","intro","ion","iron", "irony","isle","issue","ivory","ivy","jab","jabot","jackal","jade","jaded", "jaunt","jeans","jelly","jewel","jiffy","join","joker","jolly","jolt","joust", "judge","juice","jumbo","jump","jury","karma","kayak","keen","keep","kept", "kick","king","kiosk","kite","knack","knead","kneel","knelt","knife","knit", "knob","knock","knoll","knot","known","kudos","label","lace","laden","ladle", "lake","lamb","lamp","lance","land","lane","laser","latch","later","lathe", "latte","laugh","lava","layer","lead","leaf","lean","learn","lease","least", "ledge","legal","lemon","level","lever","light","lilac","limb","lime","limit", "linen","liner","liver","llama","local","lodge","lofty","logic","lone","long", "loop","lord","loss","lotus","loud","love","lower","lucky","lunar","lunch", "lure","lusty","lyric","macro","magic","major","maker","malt","mango","manor", "maple","march","marsh","mason","match","mayor","melon","mercy","merit","mesh", "metal","midst","might","mild","mill","mimic","mince","minor","minus","mirth", "mist","miter","model","money","month","moose","moral","morph","moss","motel", "motor","motto","mound","mount","mourn","mouse","movie","mower","much","mural", "murky","music","must","muted","myth","nail","name","nanny","nap","naval", "nerve","nest","never","next","niece","night","noble","noise","none","north", "notch","noted","novel","nudge","nurse","nylon","oak","oasis","obey","occur", "ocean","offer","often","olive","omega","onset","opera","orbit","organ","other", "outer","ovary","oven","owed","oxide","ozone","pace","pack","paddy","pagan", "paint","pair","palm","panda","panel","panic","papal","paper","park","party", "pasta","patch","path","patio","pause","peach","pearl","pedal","penny","perch", "peril","perky","petal","petty","phase","phone","photo","piano","pick","piece", "pilot","pinch","pine","pixel","pizza","place","plain","plane","plant","plate", "plaza","plead","pleat","plied","pluck","plumb","plume","plump","plunk","plush", "podgy","poem","point","poise","poker","polar","pond","pony","pooch","poppy", "porch","poser","pouch","pound","power","press","price","pride","prime","print", "prior","prism","privy","prize","probe","promo","prone","proof","prose","proud", "prowl","proxy","prude","prune","psalm","puck","pulse","punch","pupil","puppy", "purge","purse","pushy","quack","qualm","quart","queen","query","quest","quick", "quiet","quill","quirk","quota","quote","rabbi","race","radar","radio","raft", "rage","raid","rail","rain","raise","rally","ramp","ranch","range","rapid", "rash","ratio","raven","rayon","reach","react","realm","rebel","rebus","recap", "recon","reef","reign","relax","relay","relic","remit","renew","repay","repel", "reply","rerun","reset","resin","retro","rider","ridge","rifle","right","rigid", "rigor","rinse","riot","risen","risky","rival","river","roast","robot","rocky", "rogue","roman","roost","rope","rouge","round","route","rover","royal","rugby", "ruin","ruler","rural","rusty","sadly","saint","salad","salon","salsa","salt", "sandy","satin","sauce","sauna","savor","scale","scald","scare","scarf","scary", "scene","scent","school","score","scout","scram","scrap","screw","scrub","sedan", "sense","sepia","serve","setup","seven","shade","shaft","shake","shall","shame", "shape","share","shark","sharp","shave","sheep","sheer","shelf","shell","shift", "shine","shirt","shock","shore","short","shout","shove","shown","shrub","shrug", "sight","sigma","silly","since","sixty","size","skate","skill","skull","slain", "slam","slang","slash","slate","slave","sleek","sleep","slept","slice","slide", "slime","slope","sloth","slug","slump","smart","smash","smell","smile","smirk", "smith","smoke","snack","snake","snare","sneak","snide","sniff","snore","snout", "solar","solid","solve","sonic","south","space","spare","spark","spawn","speak", "spear","speed","spell","spend","spice","spicy","spike","spine","spoke","spoon", "sport","spray","squad","stack","staff","stage","stain","stair","stake","stale", "stall","stamp","stand","stank","stark","start","state","stave","stays","steak", "steal","steam","steel","steep","steer","stern","stick","still","sting","stink", "stock","stoic","stoke","stole","stomp","stone","stood","stool","stoop","stop", "store","stork","storm","story","stout","stove","straw","stray","strip","stuck", "study","stuff","stump","stung","stunk","stunt","sugar","suite","sulky","sunny", "super","surge","sushi","swamp","swarm","swear","sweat","sweep","sweet","swept", "swift","swill","swine","swing","swirl","swoop","sword","swore","sworn","swung", "syrup","tabby","table","tacit","taken","tally","talon","tango","taper","taste", "tasty","taunt","tease","tempo","tense","tepid","terra","text","theft","their", "theme","there","thick","thief","thigh","thing","think","third","thorn","those", "three","threw","throw","thud","thumb","tiger","tight","tilde","timer","timid", "tipsy","titan","title","toast","today","token","topic","torch","total","touch", "tough","towel","tower","toxic","trace","track","trade","trail","train","trait", "tramp","trash","trawl","treat","trend","trial","tribe","trick","tried","troop", "trout","truck","truly","trump","trunk","trust","truth","tubal","tulip","tumor", "tuner","turbo","turf","twice","twine","twist","tying","udder","ultra","umbra", "uncle","uncut","under","undid","undue","unfit","union","unite","unity","unlit", "until","upper","upset","urban","usage","usher","using","usual","utter","vague", "valid","valve","vapor","vault","venue","verse","vigor","vinyl","viola","viper", "viral","virus","visit","visor","vista","vital","vivid","vocal","vodka","vogue", "voice","volt","voter","vouch","vowel","vulse","wade","wager","wagon","waist", "waltz","warden","waste","watch","water","waver","wax","weary","weave","wedge", "weigh","weird","wheat","wheel","where","which","while","whine","whirl","whole", "widen","widow","width","wield","windy","wired","wiser","witch","woman","wood", "world","worry","worse","worst","worth","wound","wrath","wreck","wrist","wrote", "yacht","yearn","yield","young","youth","zebra","zero","zone" }; static const int WORD_COUNT = sizeof(WORDS) / sizeof(WORDS[0]); struct Config { std::string access_key; std::string secret_key; std::string bucket; std::string region; std::string endpoint; std::string admin_key; int port = 8080; }; static Config config; std::string trim(const std::string& value) { size_t start = 0; while (start < value.size() && std::isspace((unsigned char)value[start])) start++; size_t end = value.size(); while (end > start && std::isspace((unsigned char)value[end - 1])) end--; return value.substr(start, end - start); } void set_env_if_unset(const std::string& key, const std::string& value) { if (getenv(key.c_str())) return; #ifdef _WIN32 _putenv_s(key.c_str(), value.c_str()); #else setenv(key.c_str(), value.c_str(), 0); #endif } void load_dotenv(const std::string& path = ".env") { std::ifstream f(path); if (!f) return; std::string line; while (std::getline(f, line)) { line = trim(line); if (line.empty() || line[0] == '#') continue; size_t eq = line.find('='); if (eq == std::string::npos) continue; std::string key = trim(line.substr(0, eq)); std::string value = trim(line.substr(eq + 1)); if (value.size() >= 2 && ((value.front() == '"' && value.back() == '"') || (value.front() == '\'' && value.back() == '\''))) { value = value.substr(1, value.size() - 2); } if (!key.empty()) set_env_if_unset(key, value); } } std::string env_or(const char* key, const std::string& fallback = "") { const char* value = getenv(key); return value ? value : fallback; } std::string hmac_sha256(const std::string& key, const std::string& data) { unsigned int len = 32; unsigned char buf[32]; HMAC(EVP_sha256(), key.data(), key.size(), (unsigned char*)data.data(), data.size(), buf, &len); return std::string((char*)buf, len); } std::string sha256_hex(const std::string& data) { unsigned char hash[SHA256_DIGEST_LENGTH]; SHA256((unsigned char*)data.data(), data.size(), hash); std::string hex; char tmp[3]; for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) { snprintf(tmp, sizeof(tmp), "%02x", hash[i]); hex += tmp; } return hex; } std::string hex_encode(const std::string& data) { std::string hex; char tmp[3]; for (unsigned char c : data) { snprintf(tmp, sizeof(tmp), "%02x", c); hex += tmp; } return hex; } std::string url_encode(const std::string& value) { std::string result; for (unsigned char c : value) { if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') result += c; else { char buf[4]; snprintf(buf, sizeof(buf), "%%%02X", c); result += buf; } } return result; } std::string get_timestamp() { time_t now = time(nullptr); struct tm t; #ifdef _WIN32 gmtime_s(&t, &now); #else gmtime_r(&now, &t); #endif char buf[17]; strftime(buf, sizeof(buf), "%Y%m%dT%H%M%SZ", &t); return std::string(buf); } std::string generate_presigned_url(const std::string& object_key, const std::string& method, int expires = 3600) { std::string timestamp = get_timestamp(); std::string date = timestamp.substr(0, 8); std::string host = config.endpoint; std::string credential_scope = date + "/" + config.region + "/s3/aws4_request"; std::string credential = config.access_key + "/" + credential_scope; std::string canonical_uri = "/" + config.bucket; std::istringstream ss(object_key); std::string segment; while (std::getline(ss, segment, '/')) canonical_uri += "/" + url_encode(segment); std::string signed_headers = "host"; std::string canonical_querystring; canonical_querystring += "X-Amz-Algorithm=AWS4-HMAC-SHA256"; canonical_querystring += "&X-Amz-Credential=" + url_encode(credential); canonical_querystring += "&X-Amz-Date=" + timestamp; canonical_querystring += "&X-Amz-Expires=" + std::to_string(expires); canonical_querystring += "&X-Amz-SignedHeaders=" + signed_headers; std::string canonical_headers = "host:" + host + "\n"; std::string canonical_request = method + "\n" + canonical_uri + "\n" + canonical_querystring + "\n" + canonical_headers + "\n" + signed_headers + "\nUNSIGNED-PAYLOAD"; std::string string_to_sign = "AWS4-HMAC-SHA256\n" + timestamp + "\n" + credential_scope + "\n" + sha256_hex(canonical_request); std::string signing_key = hmac_sha256("AWS4" + config.secret_key, date); signing_key = hmac_sha256(signing_key, config.region); signing_key = hmac_sha256(signing_key, "s3"); signing_key = hmac_sha256(signing_key, "aws4_request"); std::string signature = hex_encode(hmac_sha256(signing_key, string_to_sign)); return "https://" + host + canonical_uri + "?" + canonical_querystring + "&X-Amz-Signature=" + signature; } std::string generate_slug() { static std::random_device rd; static std::mt19937 gen(rd()); std::uniform_int_distribution<> dis(0, WORD_COUNT - 1); std::string slug; for (int i = 0; i < 4; i++) { if (i) slug += "-"; slug += WORDS[dis(gen)]; } return slug; } std::string generate_id() { static const char chars[] = "abcdefghijklmnopqrstuvwxyz0123456789"; std::string id; static std::random_device rd; static std::mt19937 gen(rd()); std::uniform_int_distribution<> dis(0, sizeof(chars) - 2); for (int i = 0; i < 8; i++) id += chars[dis(gen)]; return id; } void save_file_meta(const std::string& slug, const std::string& filename, size_t size) { fs::create_directories("data/files"); json meta; meta["slug"] = slug; meta["filename"] = filename; meta["size"] = size; meta["uploaded_at"] = get_timestamp(); std::ofstream f("data/files/" + slug + ".json"); f << meta.dump(2); } std::string generate_token() { std::string raw; static const char hex[] = "0123456789abcdef"; static std::random_device rd; static std::mt19937 gen(rd()); std::uniform_int_distribution<> dis(0, 15); for (int i = 0; i < 32; i++) raw += hex[dis(gen)]; return sha256_hex(raw); } bool validate_token(const std::string& token) { std::string dir = "data/accounts"; if (!fs::exists(dir)) return false; for (auto& entry : fs::directory_iterator(dir)) { if (entry.path().extension() == ".json") { std::ifstream f(entry.path()); json acc; try { f >> acc; } catch (...) { continue; } if (acc.value("token", "") == token) return true; } } return false; } std::string get_token_from_request(const httplib::Request& req) { if (req.has_header("Authorization")) { std::string auth = req.get_header_value("Authorization"); if (auth.size() > 7 && auth.substr(0, 7) == "Bearer ") return auth.substr(7); } if (req.has_param("token")) return req.get_param_value("token"); return ""; } bool check_upload_auth(const httplib::Request& req, httplib::Response& res) { std::string token = get_token_from_request(req); if (token.empty() || !validate_token(token)) { res.status = 401; res.set_content("{\"error\":\"unauthorized\"}", "application/json"); return false; } return true; } std::string sign_s3_request(const std::string& method, const std::string& uri, const std::string& query, const std::string& payload, const std::string& content_type, std::string& out_auth, std::string& out_date, std::string& out_content_sha) { std::string timestamp = get_timestamp(); std::string date = timestamp.substr(0, 8); std::string host = config.endpoint; std::string credential_scope = date + "/" + config.region + "/s3/aws4_request"; out_content_sha = sha256_hex(payload); std::string signed_headers = "host;x-amz-content-sha256;x-amz-date"; std::string canonical_headers = "host:" + host + "\n" "x-amz-content-sha256:" + out_content_sha + "\n" "x-amz-date:" + timestamp + "\n"; std::string canonical_request = method + "\n" + uri + "\n" + query + "\n" + canonical_headers + "\n" + signed_headers + "\n" + out_content_sha; std::string string_to_sign = "AWS4-HMAC-SHA256\n" + timestamp + "\n" + credential_scope + "\n" + sha256_hex(canonical_request); std::string signing_key = hmac_sha256("AWS4" + config.secret_key, date); signing_key = hmac_sha256(signing_key, config.region); signing_key = hmac_sha256(signing_key, "s3"); signing_key = hmac_sha256(signing_key, "aws4_request"); std::string signature = hex_encode(hmac_sha256(signing_key, string_to_sign)); out_auth = "AWS4-HMAC-SHA256 Credential=" + config.access_key + "/" + credential_scope + ", SignedHeaders=" + signed_headers + ", Signature=" + signature; out_date = timestamp; return host; } std::string s3_initiate_multipart(const std::string& object_key, const std::string& content_type) { std::string uri = "/" + config.bucket; std::istringstream ss(object_key); std::string segment; while (std::getline(ss, segment, '/')) uri += "/" + url_encode(segment); std::string query = "uploads="; std::string auth, date, content_sha; std::string host = sign_s3_request("POST", uri, query, "", content_type, auth, date, content_sha); httplib::SSLClient cli(host); cli.set_connection_timeout(10); cli.set_read_timeout(30); cli.enable_server_certificate_verification(false); httplib::Headers headers = { {"Authorization", auth}, {"x-amz-date", date}, {"x-amz-content-sha256", content_sha}, {"Content-Type", content_type} }; auto res = cli.Post((uri + "?" + query).c_str(), headers, "", content_type.c_str()); if (res && res->status == 200) { std::regex re("([^<]+)"); std::smatch match; std::string body = res->body; if (std::regex_search(body, match, re)) return match[1].str(); } if (res) printf("[R2] initiate multipart: %d\n", res->status); else printf("[R2] initiate multipart: no response\n"); return ""; } std::string generate_presigned_part_url(const std::string& object_key, int part_number, const std::string& upload_id) { std::string timestamp = get_timestamp(); std::string date = timestamp.substr(0, 8); std::string host = config.endpoint; std::string credential_scope = date + "/" + config.region + "/s3/aws4_request"; std::string credential = config.access_key + "/" + credential_scope; std::string canonical_uri = "/" + config.bucket; std::istringstream ss(object_key); std::string segment; while (std::getline(ss, segment, '/')) canonical_uri += "/" + url_encode(segment); std::string signed_headers = "host"; std::string canonical_querystring; canonical_querystring += "X-Amz-Algorithm=AWS4-HMAC-SHA256"; canonical_querystring += "&X-Amz-Credential=" + url_encode(credential); canonical_querystring += "&X-Amz-Date=" + timestamp; canonical_querystring += "&X-Amz-Expires=3600"; canonical_querystring += "&X-Amz-SignedHeaders=" + signed_headers; canonical_querystring += "&partNumber=" + std::to_string(part_number); canonical_querystring += "&uploadId=" + url_encode(upload_id); std::string canonical_headers = "host:" + host + "\n"; std::string canonical_request = "PUT\n" + canonical_uri + "\n" + canonical_querystring + "\n" + canonical_headers + "\n" + signed_headers + "\nUNSIGNED-PAYLOAD"; std::string string_to_sign = "AWS4-HMAC-SHA256\n" + timestamp + "\n" + credential_scope + "\n" + sha256_hex(canonical_request); std::string signing_key = hmac_sha256("AWS4" + config.secret_key, date); signing_key = hmac_sha256(signing_key, config.region); signing_key = hmac_sha256(signing_key, "s3"); signing_key = hmac_sha256(signing_key, "aws4_request"); std::string signature = hex_encode(hmac_sha256(signing_key, string_to_sign)); return "https://" + host + canonical_uri + "?" + canonical_querystring + "&X-Amz-Signature=" + signature; } bool s3_complete_multipart(const std::string& object_key, const std::string& upload_id, const std::vector>& parts) { std::string uri = "/" + config.bucket; std::istringstream ss(object_key); std::string segment; while (std::getline(ss, segment, '/')) uri += "/" + url_encode(segment); std::string query = "uploadId=" + url_encode(upload_id); std::string xml = ""; for (auto& [num, etag] : parts) xml += "" + std::to_string(num) + "" + etag + ""; xml += ""; std::string content_type = "application/xml"; std::string auth, date, content_sha; std::string host = sign_s3_request("POST", uri, query, xml, content_type, auth, date, content_sha); httplib::SSLClient cli(host); cli.set_connection_timeout(10); cli.set_read_timeout(30); cli.enable_server_certificate_verification(false); httplib::Headers headers = { {"Authorization", auth}, {"x-amz-date", date}, {"x-amz-content-sha256", content_sha}, {"Content-Type", content_type} }; auto res = cli.Post((uri + "?" + query).c_str(), headers, xml, content_type.c_str()); return res && res->status == 200; } std::string serve_file(const std::string& path) { std::ifstream f(path); if (!f) return ""; return std::string((std::istreambuf_iterator(f)), std::istreambuf_iterator()); } std::string format_size(size_t bytes) { if (bytes < 1024) return std::to_string(bytes) + " B"; if (bytes < 1048576) return std::to_string(bytes / 1024) + " KB"; if (bytes < 1073741824) return std::to_string(bytes / 1048576) + " MB"; char buf[32]; snprintf(buf, sizeof(buf), "%.1f GB", bytes / 1073741824.0); return buf; } int main(int argc, char* argv[]) { load_dotenv(); config.access_key = env_or("AWS_ACCESS_KEY_ID"); config.secret_key = env_or("AWS_SECRET_ACCESS_KEY"); config.bucket = env_or("S3_BUCKET"); config.region = env_or("AWS_REGION", "auto"); config.endpoint = env_or("S3_ENDPOINT"); config.admin_key = env_or("ADMIN_KEY"); if (getenv("PORT")) config.port = std::stoi(getenv("PORT")); if (config.access_key.empty() || config.secret_key.empty() || config.bucket.empty() || config.endpoint.empty() || config.admin_key.empty()) { fprintf(stderr, "Missing required environment variables. Copy example.env to .env and fill it in.\n"); return 1; } httplib::Server svr; svr.set_payload_max_length(300 * 1024 * 1024); // Static pages with clean URLs svr.Get("/", [](const httplib::Request&, httplib::Response& res) { res.set_content(serve_file("./static/index.html"), "text/html"); }); svr.Get("/speedtest", [](const httplib::Request&, httplib::Response& res) { res.set_content(serve_file("./static/speedtest.html"), "text/html"); }); svr.Get("/login", [](const httplib::Request&, httplib::Response& res) { res.set_content(serve_file("./static/login.html"), "text/html"); }); svr.Get("/dashboard", [](const httplib::Request&, httplib::Response& res) { res.set_content(serve_file("./static/dashboard.html"), "text/html"); }); svr.Get("/terms", [](const httplib::Request&, httplib::Response& res) { res.set_content(serve_file("./static/terms.html"), "text/html"); }); svr.Get("/privacy", [](const httplib::Request&, httplib::Response& res) { res.set_content(serve_file("./static/privacy.html"), "text/html"); }); // API: admin - create account svr.Post("/api/admin/accounts", [](const httplib::Request& req, httplib::Response& res) { json body; try { body = json::parse(req.body); } catch (...) { res.status = 400; res.set_content("{\"error\":\"invalid json\"}", "application/json"); return; } if (body.value("admin_key", "") != config.admin_key) { res.status = 403; res.set_content("{\"error\":\"forbidden\"}", "application/json"); return; } std::string name = body.value("name", ""); if (name.empty()) { res.status = 400; res.set_content("{\"error\":\"name required\"}", "application/json"); return; } std::string token = generate_token(); fs::create_directories("data/accounts"); json acc; acc["name"] = name; acc["token"] = token; acc["created_at"] = get_timestamp(); std::ofstream f("data/accounts/" + generate_id() + ".json"); f << acc.dump(2); json response; response["name"] = name; response["token"] = token; res.set_content(response.dump(), "application/json"); }); // API: list files for dashboard (requires auth) svr.Get("/api/files", [](const httplib::Request& req, httplib::Response& res) { if (!check_upload_auth(req, res)) return; json files = json::array(); fs::create_directories("data/files"); for (const auto& entry : fs::directory_iterator("data/files")) { if (!entry.is_regular_file() || entry.path().extension() != ".json") continue; try { std::ifstream f(entry.path()); json meta; f >> meta; files.push_back({ {"slug", meta.value("slug", "")}, {"filename", meta.value("filename", "")}, {"size", meta.value("size", (size_t)0)}, {"uploaded_at", meta.value("uploaded_at", "")} }); } catch (...) { continue; } } std::sort(files.begin(), files.end(), [](const json& a, const json& b) { return a.value("uploaded_at", "") > b.value("uploaded_at", ""); }); json response; response["files"] = files; res.set_content(response.dump(), "application/json"); }); // API: upload (requires auth) svr.Post("/api/upload", [](const httplib::Request& req, httplib::Response& res) { if (!check_upload_auth(req, res)) return; json body; try { body = json::parse(req.body); } catch (...) { res.status = 400; res.set_content("{\"error\":\"invalid json\"}", "application/json"); return; } std::string filename = body.value("filename", ""); size_t size = body.value("size", (size_t)0); if (filename.empty()) { res.status = 400; res.set_content("{\"error\":\"filename required\"}", "application/json"); return; } std::string slug = generate_slug(); while (fs::exists("data/files/" + slug + ".json")) slug = generate_slug(); std::string object_key = slug + "/" + filename; std::string upload_url = generate_presigned_url(object_key, "PUT"); save_file_meta(slug, filename, size); json response; response["upload_url"] = upload_url; response["slug"] = slug; response["object_key"] = object_key; res.set_content(response.dump(), "application/json"); }); // API: multipart initiate (requires auth) svr.Post("/api/multipart/initiate", [](const httplib::Request& req, httplib::Response& res) { if (!check_upload_auth(req, res)) return; json body; try { body = json::parse(req.body); } catch (...) { res.status = 400; res.set_content("{\"error\":\"invalid json\"}", "application/json"); return; } std::string filename = body.value("filename", ""); size_t size = body.value("size", (size_t)0); std::string content_type = body.value("content_type", "application/octet-stream"); int num_parts = body.value("num_parts", 1); if (filename.empty()) { res.status = 400; res.set_content("{\"error\":\"filename required\"}", "application/json"); return; } std::string slug = generate_slug(); while (fs::exists("data/files/" + slug + ".json")) slug = generate_slug(); std::string object_key = slug + "/" + filename; std::string upload_id = s3_initiate_multipart(object_key, content_type); if (upload_id.empty()) { res.status = 500; res.set_content("{\"error\":\"failed to initiate multipart\"}", "application/json"); return; } json part_urls = json::array(); for (int i = 1; i <= num_parts; i++) part_urls.push_back(generate_presigned_part_url(object_key, i, upload_id)); save_file_meta(slug, filename, size); json response; response["slug"] = slug; response["upload_id"] = upload_id; response["object_key"] = object_key; response["part_urls"] = part_urls; res.set_content(response.dump(), "application/json"); }); // API: multipart complete svr.Post("/api/multipart/complete", [](const httplib::Request& req, httplib::Response& res) { json body; try { body = json::parse(req.body); } catch (...) { res.status = 400; res.set_content("{\"error\":\"invalid json\"}", "application/json"); return; } std::string object_key = body.value("object_key", ""); std::string upload_id = body.value("upload_id", ""); auto parts_json = body.value("parts", json::array()); std::vector> parts; for (auto& p : parts_json) parts.emplace_back(p["part_number"].get(), p["etag"].get()); bool ok = s3_complete_multipart(object_key, upload_id, parts); if (ok) res.set_content("{\"ok\":true}", "application/json"); else { res.status = 500; res.set_content("{\"error\":\"complete failed\"}", "application/json"); } }); // API: get download URL by slug svr.Get(R"(/api/download/(.+))", [](const httplib::Request& req, httplib::Response& res) { std::string slug = req.matches[1]; std::string meta_path = "data/files/" + slug + ".json"; if (!fs::exists(meta_path)) { res.status = 404; res.set_content("{\"error\":\"not found\"}", "application/json"); return; } std::ifstream f(meta_path); json meta; f >> meta; std::string object_key = slug + "/" + meta["filename"].get(); std::string download_url = generate_presigned_url(object_key, "GET"); json response; response["download_url"] = download_url; response["filename"] = meta["filename"]; response["size"] = meta["size"]; res.set_content(response.dump(), "application/json"); }); // API: speed test upload URL (requires auth) svr.Get(R"(/api/speedtest/upload-url/(\d+))", [](const httplib::Request& req, httplib::Response& res) { if (!check_upload_auth(req, res)) return; std::string mb = req.matches[1].str(); std::string url = generate_presigned_url("_speedtest/" + mb + "MB.bin", "PUT"); json response; response["upload_url"] = url; response["size"] = std::stoul(mb) * 1024 * 1024; res.set_content(response.dump(), "application/json"); }); // API: speed test download URL svr.Get(R"(/api/speedtest/download-url/(\d+))", [](const httplib::Request& req, httplib::Response& res) { std::string mb = req.matches[1].str(); std::string url = generate_presigned_url("_speedtest/" + mb + "MB.bin", "GET"); json response; response["download_url"] = url; response["size"] = std::stoul(mb) * 1024 * 1024; res.set_content(response.dump(), "application/json"); }); // Slug download page: /word-word-word-word svr.Get(R"(/([a-z]+-[a-z]+-[a-z]+-[a-z]+))", [](const httplib::Request& req, httplib::Response& res) { std::string slug = req.matches[1]; std::string meta_path = "data/files/" + slug + ".json"; if (!fs::exists(meta_path)) { res.status = 404; res.set_content("404

404

File not found.

Home

", "text/html"); return; } std::ifstream f(meta_path); json meta; f >> meta; std::string filename = meta["filename"].get(); size_t size = meta["size"].get(); std::string html = "" + filename + "" "" "

" + filename + "

" "

" + format_size(size) + "

" "
" "" "" ""; res.set_content(html, "text/html"); }); printf("File sharing server running on http://localhost:%d\n", config.port); svr.listen("0.0.0.0", config.port); return 0; }