first commit

This commit is contained in:
cappuch
2026-05-21 21:37:00 +01:00
commit ffa778a0ee
25 changed files with 47476 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"WebFetch(domain:raw.githubusercontent.com)",
"Bash(cmake --version)",
"Bash(g++ --version)"
]
}
}
+2
View File
@@ -0,0 +1,2 @@
.env
build/
+15
View File
@@ -0,0 +1,15 @@
cmake_minimum_required(VERSION 3.16)
project(fileshare)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(OpenSSL REQUIRED)
add_executable(fileshare src/main.cpp)
target_include_directories(fileshare PRIVATE src)
target_link_libraries(fileshare PRIVATE OpenSSL::SSL OpenSSL::Crypto)
if(WIN32)
target_link_libraries(fileshare PRIVATE ws2_32 crypt32)
endif()
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
cd "$(dirname "$0")"
mkdir -p build
cd build
cmake .. -G "Ninja" \
-DCMAKE_C_COMPILER=gcc \
-DCMAKE_CXX_COMPILER=g++ \
-DOPENSSL_ROOT_DIR="C:/msys64/ucrt64" \
-DOPENSSL_INCLUDE_DIR="C:/msys64/ucrt64/include" \
-DOPENSSL_CRYPTO_LIBRARY="C:/msys64/ucrt64/lib/libcrypto.dll.a" \
-DOPENSSL_SSL_LIBRARY="C:/msys64/ucrt64/lib/libssl.dll.a"
ninja
+6
View File
@@ -0,0 +1,6 @@
{
"filename": "mc-console-oct2014.zip",
"id": "0aff5g3t",
"size": 2217941131,
"uploaded_at": "20260521T195524Z"
}
+6
View File
@@ -0,0 +1,6 @@
{
"filename": "mc-console-oct2014.zip",
"id": "a51qod7m",
"size": 2217941131,
"uploaded_at": "20260521T193501Z"
}
+5
View File
@@ -0,0 +1,5 @@
{
"created_at": "20260521T200038Z",
"name": "mytestuser",
"token": "c19c7f4454c6b0be740132eb06f675e99800b7ef2ee9e55d38840826c7596180"
}
+6
View File
@@ -0,0 +1,6 @@
{
"filename": "1778449797HKiIQLlsjRYV6Q.mp4",
"id": "e10rdvd8",
"size": 9758894,
"uploaded_at": "20260521T192401Z"
}
+6
View File
@@ -0,0 +1,6 @@
{
"filename": "PICKY THE PICKLE .zip",
"size": 57171323,
"slug": "zero-midst-stomp-known",
"uploaded_at": "20260521T202219Z"
}
+6
View File
@@ -0,0 +1,6 @@
{
"filename": "mc-console-oct2014.zip",
"id": "nz0sjxtr",
"size": 2217941131,
"uploaded_at": "20260521T195309Z"
}
+6
View File
@@ -0,0 +1,6 @@
{
"filename": "minecraft-legacy-console-edition-source-code_archive.torrent",
"id": "p3plucyz",
"size": 28725,
"uploaded_at": "20260521T200348Z"
}
+6
View File
@@ -0,0 +1,6 @@
{
"filename": "mc-cpp-snapshot.zip",
"id": "u4hhscc0",
"size": 20606505,
"uploaded_at": "20260521T200105Z"
}
+6
View File
@@ -0,0 +1,6 @@
{
"filename": "mc-console-oct2014.zip",
"id": "uavsqdin",
"size": 2217941131,
"uploaded_at": "20260521T193423Z"
}
+6
View File
@@ -0,0 +1,6 @@
{
"filename": "minecraft-legacy-console-edition-source-code_archive.torrent",
"id": "ymmw5usc",
"size": 28725,
"uploaded_at": "20260521T200742Z"
}
+11
View File
@@ -0,0 +1,11 @@
PORT=8080
# Cloudflare R2 S3 API credentials.
AWS_ACCESS_KEY_ID=your_r2_access_key_id
AWS_SECRET_ACCESS_KEY=your_r2_secret_access_key
AWS_REGION=auto
S3_BUCKET=your_r2_bucket_name
S3_ENDPOINT=your_account_id.r2.cloudflarestorage.com
# Pick a long random value for account creation.
ADMIN_KEY=change_me_to_a_long_random_admin_key
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
cd "$(dirname "$0")"
./build/fileshare.exe
+20142
View File
File diff suppressed because it is too large Load Diff
+26073
View File
File diff suppressed because it is too large Load Diff
+773
View File
@@ -0,0 +1,773 @@
#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;
}
+143
View File
@@ -0,0 +1,143 @@
<!DOCTYPE html>
<html>
<head>
<title>Dashboard - fileshares.cfd</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:monospace;padding:40px;background:white;color:black}
h1{margin-bottom:20px}
.box{border:1px solid black;padding:20px;max-width:620px;margin-bottom:20px}
button{border:1px solid black;background:white;padding:8px 16px;cursor:pointer;font-family:monospace}
button:hover{background:black;color:white}
input[type="file"]{margin-bottom:10px;display:block}
#status{margin-top:10px}
#progress-wrap{display:none;margin-top:10px}
#progress-bar{width:100%;height:20px;border:1px solid black}
#progress-fill{height:100%;background:black;width:0%}
#progress-text{margin-top:4px}
#link{margin-top:10px;word-break:break-all}
#link a,.files a{color:black;font-weight:bold}
.files{max-width:620px}
.file{border-top:1px solid black;padding:10px 0}
.file:first-child{border-top:0}
.meta{font-size:12px;margin-top:4px}
.empty{margin-top:10px}
.nav{margin-top:40px;font-size:12px}
a{color:black}
</style>
</head>
<body>
<h1>Dashboard</h1>
<div class="box">
<input type="file" id="fileInput">
<button onclick="upload()">Upload</button>
<div id="status"></div>
<div id="progress-wrap">
<div id="progress-bar"><div id="progress-fill"></div></div>
<div id="progress-text"></div>
</div>
<div id="link"></div>
</div>
<div class="box files">
<h2>Files</h2>
<div id="files" class="empty">Loading...</div>
</div>
<div class="nav"><a href="/">Home</a> | <a href="/speedtest">Speed Test</a> | <a href="/dashboard">Dashboard</a> | <a href="/terms">Terms</a> | <a href="/privacy">Privacy</a></div>
<script>
function getToken(){return localStorage.getItem('upload_token')||''}
function authHeaders(){return{'Authorization':'Bearer '+getToken()}}
function formatBytes(b){if(b<1024)return b+' B';if(b<1048576)return(b/1024).toFixed(1)+' KB';if(b<1073741824)return(b/1048576).toFixed(1)+' MB';return(b/1073741824).toFixed(2)+' GB'}
function formatTime(s){if(s<60)return Math.ceil(s)+'s';if(s<3600)return Math.floor(s/60)+'m '+Math.ceil(s%60)+'s';return Math.floor(s/3600)+'h '+Math.floor((s%3600)/60)+'m'}
function formatDate(s){const d=new Date(s);return isNaN(d)?s:d.toLocaleString()}
const CHUNK_SIZE=25*1024*1024;
const MAX_CONCURRENT=8;
if(!getToken()){
document.getElementById('status').innerHTML='No token set. <a href="/login">Login first</a>.';
document.getElementById('files').innerHTML='Login to see your files.';
}else{
loadFiles();
}
async function loadFiles(){
const files=document.getElementById('files');
files.textContent='Loading...';
const resp=await fetch('/api/files',{headers:authHeaders()});
const data=await resp.json();
if(!resp.ok){files.textContent='Could not load files.';return}
if(!data.files.length){files.textContent='No files uploaded yet.';return}
files.className='';
files.innerHTML=data.files.map(file=>{
const url=location.origin+'/'+file.slug;
return '<div class="file"><a href="/'+file.slug+'">'+escapeHtml(file.filename)+'</a><div class="meta">'+formatBytes(file.size)+' | '+formatDate(file.uploaded_at)+' | <a href="'+url+'">'+url+'</a></div></div>';
}).join('');
}
function escapeHtml(value){
return String(value).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
async function upload(){
const input=document.getElementById('fileInput');
const status=document.getElementById('status');
const wrap=document.getElementById('progress-wrap');
const fill=document.getElementById('progress-fill');
const text=document.getElementById('progress-text');
const link=document.getElementById('link');
link.innerHTML='';
if(!input.files.length){status.textContent='No file selected';return}
if(!getToken()){status.innerHTML='No token set. <a href="/login">Login first</a>.';return}
const file=input.files[0];
if(file.size<10*1024*1024) await uploadSimple(file,status,wrap,fill,text,link);
else await uploadMultipart(file,status,wrap,fill,text,link);
}
async function uploadSimple(file,status,wrap,fill,text,link){
status.textContent='Requesting upload URL...';
wrap.style.display='none';
const resp=await fetch('/api/upload',{method:'POST',headers:{'Content-Type':'application/json',...authHeaders()},body:JSON.stringify({filename:file.name,size:file.size})});
const data=await resp.json();
if(!resp.ok){status.textContent='Error: '+data.error;return}
status.textContent='Uploading...';
wrap.style.display='block';fill.style.width='0%';
const startTime=Date.now();
await new Promise((resolve,reject)=>{
const xhr=new XMLHttpRequest();
xhr.open('PUT',data.upload_url);
xhr.upload.onprogress=function(e){if(!e.lengthComputable)return;const pct=(e.loaded/e.total)*100;fill.style.width=pct+'%';const elapsed=(Date.now()-startTime)/1000;const speed=e.loaded/elapsed;text.textContent=pct.toFixed(1)+'% | '+formatBytes(e.loaded)+' / '+formatBytes(e.total)+' | '+formatBytes(speed)+'/s | ETA '+formatTime((e.total-e.loaded)/speed)};
xhr.onload=function(){if(xhr.status>=200&&xhr.status<300){fill.style.width='100%';const elapsed=(Date.now()-startTime)/1000;text.textContent='100% | '+formatBytes(file.size)+' in '+formatTime(elapsed)+' | avg '+formatBytes(file.size/elapsed)+'/s';status.textContent='Done!';link.innerHTML='<a href="/'+data.slug+'">'+location.origin+'/'+data.slug+'</a>';loadFiles();resolve()}else{status.textContent='Upload failed: '+xhr.status;reject()}};
xhr.onerror=function(){status.textContent='Upload failed: network error';reject()};
xhr.send(file);
});
}
async function uploadMultipart(file,status,wrap,fill,text,link){
const numParts=Math.ceil(file.size/CHUNK_SIZE);
status.textContent='Initiating multipart ('+numParts+' parts)...';
wrap.style.display='none';
const resp=await fetch('/api/multipart/initiate',{method:'POST',headers:{'Content-Type':'application/json',...authHeaders()},body:JSON.stringify({filename:file.name,size:file.size,content_type:file.type||'application/octet-stream',num_parts:numParts})});
const data=await resp.json();
if(!resp.ok){status.textContent='Error: '+data.error;return}
status.textContent='Uploading '+numParts+' parts...';
wrap.style.display='block';fill.style.width='0%';
const startTime=Date.now();
const partProgress=new Array(numParts).fill(0);
const completedParts=[];
let failed=false;
function updateProgress(){const loaded=partProgress.reduce((a,b)=>a+b,0);const pct=(loaded/file.size)*100;fill.style.width=pct+'%';const elapsed=(Date.now()-startTime)/1000;const speed=loaded/elapsed;text.textContent=pct.toFixed(1)+'% | '+formatBytes(loaded)+' / '+formatBytes(file.size)+' | '+formatBytes(speed)+'/s | ETA '+formatTime((file.size-loaded)/speed)}
function uploadPart(i){return new Promise((resolve,reject)=>{const start=i*CHUNK_SIZE;const end=Math.min(start+CHUNK_SIZE,file.size);const chunk=file.slice(start,end);const xhr=new XMLHttpRequest();xhr.open('PUT',data.part_urls[i]);xhr.upload.onprogress=function(e){if(e.lengthComputable){partProgress[i]=e.loaded;updateProgress()}};xhr.onload=function(){if(xhr.status>=200&&xhr.status<300){partProgress[i]=end-start;completedParts.push({part_number:i+1,etag:xhr.getResponseHeader('ETag')});updateProgress();resolve()}else reject(new Error('Part '+(i+1)+' failed'))};xhr.onerror=function(){reject(new Error('Part '+(i+1)+' network error'))};xhr.send(chunk)})}
const queue=Array.from({length:numParts},(_,i)=>i);
const workers=[];
for(let i=0;i<MAX_CONCURRENT;i++){workers.push((async()=>{while(queue.length&&!failed){const idx=queue.shift();try{await uploadPart(idx)}catch(e){failed=true;status.textContent=e.message}}})())}
await Promise.all(workers);
if(failed)return;
status.textContent='Finalizing...';
completedParts.sort((a,b)=>a.part_number-b.part_number);
const complete=await fetch('/api/multipart/complete',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({object_key:data.object_key,upload_id:data.upload_id,parts:completedParts})});
if(complete.ok){const elapsed=(Date.now()-startTime)/1000;fill.style.width='100%';text.textContent='100% | '+formatBytes(file.size)+' in '+formatTime(elapsed)+' | avg '+formatBytes(file.size/elapsed)+'/s';status.textContent='Done!';link.innerHTML='<a href="/'+data.slug+'">'+location.origin+'/'+data.slug+'</a>';loadFiles()}
else status.textContent='Failed to finalize upload';
}
</script>
</body>
</html>
+38
View File
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<title>fileshares.cfd</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:monospace;padding:40px;background:white;color:black;max-width:680px}
h1{margin-bottom:12px;font-size:32px}
p{margin-bottom:12px;line-height:1.5}
.actions{margin-top:24px}
a.button{display:inline-block;border:1px solid black;background:white;color:black;padding:8px 16px;text-decoration:none;margin-right:8px}
a.button:hover{background:black;color:white}
.nav{margin-top:40px;font-size:12px}
a{color:black}
</style>
</head>
<body>
<h1>fileshares.cfd</h1>
<p>Simple, fast file hosting without the bloat.</p>
<p>Upload a file, get a clean link, and come back to your dashboard whenever you need older uploads. Access is request-only, so you will need an upload token before you can use the dashboard.</p>
<div class="actions">
<a class="button" id="mainAction" href="/login">Login</a>
<a class="button" href="/speedtest">Speed Test</a>
</div>
<div class="nav"><a href="/">Home</a> | <a href="/speedtest">Speed Test</a> | <a id="navAuth" href="/login">Login</a> | <a href="/terms">Terms</a> | <a href="/privacy">Privacy</a></div>
<script>
function hasToken(){return !!localStorage.getItem('upload_token')}
const main=document.getElementById('mainAction');
const nav=document.getElementById('navAuth');
if(hasToken()){
main.href='/dashboard';
main.textContent='Dashboard';
nav.href='/dashboard';
nav.textContent='Dashboard';
}
</script>
</body>
</html>
+40
View File
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:monospace;padding:40px;background:white;color:black}
h1{margin-bottom:20px}
.box{border:1px solid black;padding:20px;max-width:500px}
button{border:1px solid black;background:white;padding:8px 16px;cursor:pointer;font-family:monospace}
button:hover{background:black;color:white}
input[type="text"]{width:100%;font-family:monospace;border:1px solid black;padding:8px;margin-bottom:10px}
#status{margin-top:10px}
.nav{margin-top:40px;font-size:12px}
a{color:black}
</style>
</head>
<body>
<h1>Login</h1>
<div class="box">
<p style="margin-bottom:10px">Enter your upload token:</p>
<input type="text" id="tokenInput" placeholder="Paste token here">
<button onclick="save()">Save</button>
<button onclick="clear_()">Clear</button>
<div id="status"></div>
</div>
<div class="nav"><a href="/">Home</a> | <a href="/speedtest">Speed Test</a> | <a id="navAuth" href="/login">Login</a> | <a href="/terms">Terms</a> | <a href="/privacy">Privacy</a></div>
<script>
const input=document.getElementById('tokenInput');
const status=document.getElementById('status');
const nav=document.getElementById('navAuth');
const saved=localStorage.getItem('upload_token');
function updateNav(){if(localStorage.getItem('upload_token')){nav.href='/dashboard';nav.textContent='Dashboard'}else{nav.href='/login';nav.textContent='Login'}}
if(saved){input.value=saved;status.textContent='Token loaded from storage.'}
updateNav();
function save(){const t=input.value.trim();if(t){localStorage.setItem('upload_token',t);status.innerHTML='Saved. <a href="/dashboard">Open dashboard</a>.';updateNav()}else status.textContent='Enter a token first.'}
function clear_(){localStorage.removeItem('upload_token');input.value='';status.textContent='Cleared.';updateNav()}
</script>
</body>
</html>
+28
View File
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<title>Privacy Policy</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:monospace;padding:40px;background:white;color:black;max-width:600px}
h1{margin-bottom:20px}
p{margin-bottom:10px}
.nav{margin-top:40px;font-size:12px}
a{color:black}
</style>
</head>
<body>
<h1>Privacy Policy</h1>
<p>We keep this simple.</p>
<p>1. Uploaded files are stored on our servers.</p>
<p>2. File metadata, including name, size, and upload time, is stored so the service can show and serve your files.</p>
<p>3. No cookies are used.</p>
<p>4. Authentication tokens are saved in your browser.</p>
<p>5. No analytics or tracking.</p>
<div class="nav"><a href="/">Home</a> | <a href="/speedtest">Speed Test</a> | <a id="navAuth" href="/login">Login</a> | <a href="/terms">Terms</a> | <a href="/privacy">Privacy</a></div>
<script>
const nav=document.getElementById('navAuth');
if(localStorage.getItem('upload_token')){nav.href='/dashboard';nav.textContent='Dashboard'}
</script>
</body>
</html>
+100
View File
@@ -0,0 +1,100 @@
<!DOCTYPE html>
<html>
<head>
<title>Speed Test</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:monospace;padding:40px;background:white;color:black}
h1{margin-bottom:20px}
.box{border:1px solid black;padding:20px;max-width:500px}
button{border:1px solid black;background:white;padding:8px 16px;cursor:pointer;font-family:monospace;margin:2px}
button:hover{background:black;color:white}
#status{margin-top:10px}
#progress-wrap{display:none;margin-top:10px}
#bar{width:100%;height:20px;border:1px solid black}
#fill{height:100%;background:black;width:0%}
#text{margin-top:4px}
.nav{margin-top:40px;font-size:12px}
a{color:black}
.section{margin-bottom:10px}
</style>
</head>
<body>
<h1>Speed Test</h1>
<div class="box">
<div class="section">
<b>Download:</b><br>
<button onclick="testDown(10)">10 MB</button>
<button onclick="testDown(50)">50 MB</button>
<button onclick="testDown(100)">100 MB</button>
<button onclick="testDown(250)">250 MB</button>
</div>
<div class="section" id="uploadSection" style="display:none">
<b>Upload:</b><br>
<button onclick="testUp(10)">10 MB</button>
<button onclick="testUp(50)">50 MB</button>
<button onclick="testUp(100)">100 MB</button>
<button onclick="testUp(250)">250 MB</button>
</div>
<div id="status"></div>
<div id="progress-wrap">
<div id="bar"><div id="fill"></div></div>
<div id="text"></div>
</div>
</div>
<div class="nav"><a href="/">Home</a> | <a href="/speedtest">Speed Test</a> | <a id="navAuth" href="/login">Login</a> | <a href="/terms">Terms</a> | <a href="/privacy">Privacy</a></div>
<script>
function getToken(){return localStorage.getItem('upload_token')||''}
function authHeaders(){return{'Authorization':'Bearer '+getToken()}}
function formatBytes(b){if(b<1024)return b+' B';if(b<1048576)return(b/1024).toFixed(1)+' KB';if(b<1073741824)return(b/1048576).toFixed(1)+' MB';return(b/1073741824).toFixed(2)+' GB'}
function formatTime(s){if(s<60)return Math.ceil(s)+'s';if(s<3600)return Math.floor(s/60)+'m '+Math.ceil(s%60)+'s';return Math.floor(s/3600)+'h '+Math.floor((s%3600)/60)+'m'}
const nav=document.getElementById('navAuth');
if(getToken()){
nav.href='/dashboard';
nav.textContent='Dashboard';
document.getElementById('uploadSection').style.display='block';
}
async function testDown(mb){
const status=document.getElementById('status');
const wrap=document.getElementById('progress-wrap');
const fill=document.getElementById('fill');
const text=document.getElementById('text');
const total=mb*1024*1024;
status.textContent='Getting download URL...';wrap.style.display='block';fill.style.width='0%';text.textContent='';
const resp=await fetch('/api/speedtest/download-url/'+mb);
const data=await resp.json();
status.textContent='Downloading '+mb+' MB...';
const xhr=new XMLHttpRequest();
xhr.open('GET',data.download_url);xhr.responseType='blob';
const start=Date.now();
xhr.onprogress=function(e){if(!e.lengthComputable)return;const pct=(e.loaded/e.total)*100;fill.style.width=pct+'%';const elapsed=(Date.now()-start)/1000;const speed=e.loaded/elapsed;text.textContent=pct.toFixed(1)+'% | '+formatBytes(e.loaded)+' / '+formatBytes(e.total)+' | '+formatBytes(speed)+'/s'};
xhr.onload=function(){if(xhr.status>=200&&xhr.status<300){const elapsed=(Date.now()-start)/1000;const speed=total/elapsed;fill.style.width='100%';status.textContent='Download: '+formatBytes(speed)+'/s ('+mb+' MB in '+formatTime(elapsed)+')';text.textContent='100%'}else{status.textContent='Download failed: '+xhr.status+' (upload test data first)'}};
xhr.onerror=function(){status.textContent='Download failed (upload test data for this size first)'};
xhr.send();
}
async function testUp(mb){
const status=document.getElementById('status');
const wrap=document.getElementById('progress-wrap');
const fill=document.getElementById('fill');
const text=document.getElementById('text');
const total=mb*1024*1024;
if(!getToken()){status.textContent='No token. Go to Login page first.';return}
status.textContent='Getting upload URL...';wrap.style.display='block';fill.style.width='0%';text.textContent='';
const resp=await fetch('/api/speedtest/upload-url/'+mb,{headers:authHeaders()});
const data=await resp.json();
if(!resp.ok){status.textContent='Error: '+(data.error||'unauthorized');return}
status.textContent='Uploading '+mb+' MB...';
const blob=new Blob([new ArrayBuffer(total)]);
const xhr=new XMLHttpRequest();
xhr.open('PUT',data.upload_url);
const start=Date.now();
xhr.upload.onprogress=function(e){if(!e.lengthComputable)return;const pct=(e.loaded/e.total)*100;fill.style.width=pct+'%';const elapsed=(Date.now()-start)/1000;const speed=e.loaded/elapsed;text.textContent=pct.toFixed(1)+'% | '+formatBytes(e.loaded)+' / '+formatBytes(e.total)+' | '+formatBytes(speed)+'/s | ETA '+formatTime((e.total-e.loaded)/speed)};
xhr.onload=function(){if(xhr.status>=200&&xhr.status<300){const elapsed=(Date.now()-start)/1000;const speed=total/elapsed;fill.style.width='100%';status.textContent='Upload: '+formatBytes(speed)+'/s ('+mb+' MB in '+formatTime(elapsed)+')';text.textContent='100%'}else{status.textContent='Upload failed: '+xhr.status}};
xhr.onerror=function(){status.textContent='Upload failed: network error'};
xhr.send(blob);
}
</script>
</body>
</html>
+28
View File
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<title>Terms of Service</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:monospace;padding:40px;background:white;color:black;max-width:600px}
h1{margin-bottom:20px}
p{margin-bottom:10px}
.nav{margin-top:40px;font-size:12px}
a{color:black}
</style>
</head>
<body>
<h1>Terms of Service</h1>
<p>By using this service, you agree to the following:</p>
<p>1. Do not upload illegal content.</p>
<p>2. Do not abuse the service.</p>
<p>3. Files may be removed at any time without notice.</p>
<p>4. No warranty is provided. Use at your own risk.</p>
<p>5. We reserve the right to terminate access at any time.</p>
<div class="nav"><a href="/">Home</a> | <a href="/speedtest">Speed Test</a> | <a id="navAuth" href="/login">Login</a> | <a href="/terms">Terms</a> | <a href="/privacy">Privacy</a></div>
<script>
const nav=document.getElementById('navAuth');
if(localStorage.getItem('upload_token')){nav.href='/dashboard';nav.textContent='Dashboard'}
</script>
</body>
</html>