Files
fileshare/src/main.cpp
T
2026-05-21 21:37:00 +01:00

774 lines
37 KiB
C++

#define CPPHTTPLIB_OPENSSL_SUPPORT
#include "httplib.h"
#include "json.hpp"
#include <string>
#include <ctime>
#include <fstream>
#include <sstream>
#include <algorithm>
#include <vector>
#include <filesystem>
#include <regex>
#include <random>
#include <cstdlib>
#include <openssl/hmac.h>
#include <openssl/sha.h>
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("<UploadId>([^<]+)</UploadId>");
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<std::pair<int, std::string>>& 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 = "<CompleteMultipartUpload>";
for (auto& [num, etag] : parts)
xml += "<Part><PartNumber>" + std::to_string(num) + "</PartNumber><ETag>" + etag + "</ETag></Part>";
xml += "</CompleteMultipartUpload>";
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<char>(f)), std::istreambuf_iterator<char>());
}
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<std::pair<int, std::string>> parts;
for (auto& p : parts_json)
parts.emplace_back(p["part_number"].get<int>(), p["etag"].get<std::string>());
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>();
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("<html><head><title>404</title><style>*{margin:0;padding:0}body{font-family:monospace;padding:40px}</style></head><body><h1>404</h1><p>File not found.</p><p><a href=\"/\">Home</a></p></body></html>", "text/html");
return;
}
std::ifstream f(meta_path); json meta; f >> meta;
std::string filename = meta["filename"].get<std::string>();
size_t size = meta["size"].get<size_t>();
std::string html = "<!DOCTYPE html><html><head><title>" + filename + "</title>"
"<style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:monospace;padding:40px;background:white;color:black}"
".box{border:1px solid black;padding:20px;max-width:500px}"
"button{border:1px solid black;background:white;padding:10px 20px;cursor:pointer;font-family:monospace;font-size:16px}"
"button:hover{background:black;color:white}"
".nav{margin-top:40px;font-size:12px}a{color:black}</style></head><body>"
"<div class=\"box\"><h2>" + filename + "</h2>"
"<p style=\"margin:10px 0\">" + format_size(size) + "</p>"
"<button onclick=\"dl()\">Download</button></div>"
"<div class=\"nav\"><a href=\"/\">Home</a> | <a href=\"/terms\">Terms</a> | <a href=\"/privacy\">Privacy</a></div>"
"<script>function dl(){fetch('/api/download/" + slug + "').then(r=>r.json()).then(d=>window.location=d.download_url)}</script>"
"</body></html>";
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;
}