diff options
| author | 2026-06-24 03:22:20 -0400 | |
|---|---|---|
| committer | 2026-06-24 03:22:20 -0400 | |
| commit | 1246a277c17d8b600f9c9b1de7a12ae6fb6da962 (patch) | |
| tree | 9703871809d1f799c1e690f8fd11805c8b8b0f66 | |
init
| -rw-r--r-- | .config.php.example | 7 | ||||
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | index.html | 56 | ||||
| -rw-r--r-- | static/script.js | 178 | ||||
| -rw-r--r-- | static/style.css | 55 | ||||
| -rw-r--r-- | upload.php | 95 |
6 files changed, 392 insertions, 0 deletions
diff --git a/.config.php.example b/.config.php.example new file mode 100644 index 0000000..dd6d3fe --- /dev/null +++ b/.config.php.example @@ -0,0 +1,7 @@ +<?php +error_reporting(E_ALL); + +define('UPLOAD_URL', 'https://g.hostfil.es/'); +define('UPLOAD_DIR', '/var/www/files/'); +define('NOTIFICATION_ADDRESS', '127.0.0.1'); +define('NOTIFICATION_PORT', 1234); diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b0204da --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.config.php diff --git a/index.html b/index.html new file mode 100644 index 0000000..a7c3de1 --- /dev/null +++ b/index.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<html> +<head> + <title>hostfil.es</title> + <link rel="stylesheet" href="/static/style.css" /> + <script src="/static/script.js"></script> +</head> +<body> +<form action="upload.php" method="POST" enctype="multipart/form-data"> + +<label id="drop-zone"> + Drop images here, or click to upload. + <input type="file" id="file-input" multiple accept="*/*" name="file[]" /> +</label> +<ul id="preview"></ul> +<noscript><input type="submit" value="Upload" /></noscript> + +<section> + <h2 class="expander-toggle">FAQ <span class="expander-icon">►</span></h2> + <div class="expander expander-hidden"> + <dl> + <dt>What type of files are allowed?</dt> + <dd>Anything legal. Encrypted or obfuscated stuff will be deleted swiftly.</dd> + + <dt>What's the max file size?</dt> + <dd>192MB, I think. Let me know if it's not.</dd> + + <dt>How long do files stay?</dt> + <dd>I dunno. Til I feel like deleting them, which will depend how big they are. Probably for a while. I have a <a href="/upload.php?df">decent amount</a> of free space online (and even more offline that I might move items to for cold storage). + <br />Use it more and make me cry.</dd> + + <dt>Are my files private?</dt> + <dd>Mostly. Anyone who knows the SHA-512 sum of the contents can probably find them. Any of our staff (admins and moderators) can find them. And our server host (OVH) could also access our server and find them.</dd> + + <dt>Do you keep logs?</dt> + <dd>Of uploads only. File downloads are not logged, and will be supported via Tor Hidden Service and I2P eep-site in the near future. The only statistics we retain are related to aggregate, server-wide resource usage.</dd> + + <dt>How do I report abuse?</dt> + <dd>Email abuse@hostfil.es</dd> + + <dt>Other contact methods?</dt> + <dd><a href="ircs://irc.1459.io:6697/#1459">IRC</a> (<a href="https://irc.1459.io/">More info</a>)</dd> + + <dt>Why?</dt> + <dd>Cuz.</dd> + + <dt>Who should I thank?</dt> + <dd><a href="https://steeri.ng/">steering</a>.</dt> + </dl> + + </div> +</section> + +</form> +</body> +</html> diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..5d48603 --- /dev/null +++ b/static/script.js @@ -0,0 +1,178 @@ +/* +sources: +MDN: + https://developer.mozilla.org/en-US/docs/Web/API/File_API/Using_files_from_web_applications#example_uploading_a_user-selected_file + https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop +web.dev: + https://web.dev/patterns/clipboard/paste-files/ +note that this likely means the code is mixed-licensed under: + MDN: "Attributions and copyright licensing" by Mozilla Contributors, licensed under CC-BY-SA 2.5. + web.dev: code samples are licensed under the Apache 2.0 License. For details, see the Google Developers Site Policies +*/ + +addEventListener("DOMContentLoaded", function () { + function uploadFiles(files) { + for (const file of files) { + const li = document.createElement("li"); + if (file.type.startsWith("image/")) { + const img = document.createElement("img"); + img.src = URL.createObjectURL(file); + img.alt = file.name; + li.appendChild(img); + } + li.appendChild(document.createTextNode("Uploading " + file.name + "...")); + preview.appendChild(li); + + new FileUpload(li, file) + } + } + + /* DRAG-N-DROP */ + const dropZone = document.getElementById("drop-zone"); + const preview = document.getElementById("preview"); + + function dropHandler(ev) { + ev.preventDefault(); + const files = [...ev.dataTransfer.items] + .map((item) => item.getAsFile()) + .filter((file) => file); + uploadFiles(files); + } + + dropZone.addEventListener("drop", dropHandler); + + dropZone.addEventListener("dragover", (e) => { + const fileItems = [...e.dataTransfer.items].filter( + (item) => item.kind === "file", + ); + if (fileItems.length > 0) { + e.preventDefault(); + if (fileItems.some((item) => item.type.startsWith("image/"))) { + e.dataTransfer.dropEffect = "copy"; + } else { + e.dataTransfer.dropEffect = "none"; + } + } + }); + + window.addEventListener("dragover", (e) => { + const fileItems = [...e.dataTransfer.items].filter( + (item) => item.kind === "file", + ); + if (fileItems.length > 0) { + e.preventDefault(); + if (!dropZone.contains(e.target)) { + e.dataTransfer.dropEffect = "none"; + } + } + }); + + const fileInput = document.getElementById("file-input"); + fileInput.addEventListener("change", (e) => { + uploadFiles(e.target.files); + }); + + window.addEventListener("drop", (e) => { + if ([...e.dataTransfer.items].some((item) => item.kind === "file")) { + e.preventDefault(); + } + }); + + + + /* PASTE */ + document.addEventListener('paste', async (e) => { + // Prevent the default behavior, so you can code your own logic. + e.preventDefault(); + if (!e.clipboardData.files.length) { + return; + } + // Iterate over all pasted files. + uploadFiles(e.clipboardData.files) + }); + + + + /* EXPANDER */ + document.querySelectorAll('.expander-toggle').forEach((e) => { + e.addEventListener('click', toggle_expand); + }); +}); + +function toggle_expand(ev) { + const cl = this.parentNode.querySelector('.expander').classList; + const icon = this.parentNode.querySelector('.expander-icon'); + if (cl.contains('expander-hidden')) { + cl.replace('expander-hidden', 'expander-shown'); + icon.innerHTML = "▼"; + } else { + cl.remove('expander-shown'); + cl.add('expander-hidden'); + icon.innerHTML = "►"; + } +} + +function FileUpload(preview, file) { + this.ctrl = createThrobber(preview); + const xhr = new XMLHttpRequest(); + this.xhr = xhr; + + this.xhr.upload.addEventListener("progress", (e) => { + if (e.lengthComputable) { + const percentage = Math.round((e.loaded * 100) / e.total); + this.ctrl.update(percentage); + } + }); + + xhr.upload.addEventListener("load", (e) => { + this.ctrl.update(100); + preview.removeChild(this.ctrl); + }); + xhr.addEventListener("load", (e) => { + const a = document.createElement('a'); + const text = document.createTextNode(xhr.responseText); + a.href = xhr.responseText; + a.appendChild(text); + preview.appendChild(a); + }); + xhr.open("POST", ascend(preview, "form").action + "/" + file.name); + xhr.overrideMimeType("application/octet-stream; charset=x-user-defined-binary"); + xhr.send(file); +} + +function createThrobber(element) { + const throbberWidth = 64; + const throbberHeight = 6; + const throbber = document.createElement("canvas"); + throbber.classList.add("upload-progress"); + throbber.setAttribute("width", throbberWidth); + throbber.setAttribute("height", throbberHeight); + element.appendChild(throbber); + throbber.ctx = throbber.getContext("2d"); + throbber.ctx.fillStyle = "orange"; + throbber.update = (percent) => { + throbber.ctx.fillRect( + 0, + 0, + (throbberWidth * percent) / 100, + throbberHeight, + ); + if (percent === 100) { + throbber.ctx.fillStyle = "green"; + } + }; + throbber.update(0); + return throbber; +} + +function ascend(from_element, to_tag_name) { + to_tag_name = to_tag_name.toLowerCase(); + e = from_element; + + while (e && e.parentNode) { + e = e.parentNode; + if (e.tagName && e.tagName.toLowerCase() == to_tag_name) { + return e; + } + } +} diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..b2584d7 --- /dev/null +++ b/static/style.css @@ -0,0 +1,55 @@ +body { + font-family: "Arial", sans-serif; +} + +#drop-zone { + display: flex; + align-items: center; + justify-content: center; + width: 500px; + max-width: 100%; + height: 200px; + padding: 1em; + border: 1px solid #cccccc; + border-radius: 4px; + color: slategray; + cursor: pointer; +} + +#file-input { + display: none; +} + +#preview { + display: flex; + flex-direction: column; + gap: 0.5em; + list-style: none; + padding: 0; +} + +#preview li { + display: flex; + align-items: center; + gap: 0.5em; + margin: 0; + width: 100%; + height: 100px; +} + +#preview img { + width: 100px; + height: 100px; + object-fit: cover; +} + +.upload-progress { + border: 1px solid black; +} + +.expander-hidden { + display: none; +} +.expander-toggle { + cursor: pointer; +} diff --git a/upload.php b/upload.php new file mode 100644 index 0000000..3c0c3a7 --- /dev/null +++ b/upload.php @@ -0,0 +1,95 @@ +<?php +require(__DIR__ . '/.config.php'); +header('Content-Type: text/plain'); + +if (isset($_GET['df'])) { + system("df -h --output=avail / | tail -1"); + exit; +} + +$c_t = strtolower($_SERVER['HTTP_CONTENT_TYPE'] ?? ''); +if ($c_t == 'multipart/form-data' || 0 === strpos($c_t, 'multipart/form-data;')) { + foreach (array_keys($_FILES['file']['size']) as $index) { + // remap the array to have the dimensions ordered properly. + $file = array(); + foreach (array_keys($_FILES['file']) as $key) { + $file[$key] = $_FILES['file'][$key][$index]; + } + // good, now $file['tmp_name'] exists for example, instead of $_FILES['file']['tmp_name'][$index]... + + if (!is_uploaded_file($file['tmp_name'])) { + die_error("Not an uploaded file: $file[tmp_name]"); + } + $hash = hash_file("sha512", $file['tmp_name']); + if (FALSE === $hash) { + die_error("Unable to hash uploaded file?!"); + } + $filename = $hash . get_extension($file['name']); + $filepath = UPLOAD_DIR . '/' . $filename; + if (FALSE === move_uploaded_file($file['tmp_name'], $filepath)) { + die_error("Renaming uploaded file $file[tmp_name] to $filepath failed"); + } + echo UPLOAD_URL . $filename . "\n"; + send_notification("[hostfil.es] $_SERVER[REMOTE_ADDR] uploaded $file[full_path] $file[type] -> $filename"); + } +} else { + $in_fh = fopen('php://input', 'r'); + $tmp_path = tempnam(UPLOAD_DIR, '.up-'); + $out_fh = fopen($tmp_path, 'w'); + if (FALSE === $out_fh) { + die_error("Couldn't open temporary file $tmp_path"); + } + $hash = hash_init("sha512"); + $size = 0; + while (!feof($in_fh) && FALSE !== ($data = fread($in_fh, 1024*1024))) { + hash_update($hash, $data); + $fwrite_status = fwrite($out_fh, $data); + if (FALSE === $fwrite_status || $fwrite_status < strlen($data)) { + die_error("Outputting to temporary file $tmp_path failed"); + } + $size += $fwrite_status; + } + fclose($in_fh); + if (FALSE === fclose($out_fh)) { + die_error("Closing temporary file $tmp_path failed"); + } + $filename = hash_final($hash) . get_extension($_SERVER['PATH_INFO']); + $filepath = UPLOAD_DIR . '/' . $filename; + if (file_exists($filepath)) { + unlink($tmp_path); + } else { + if (FALSE === rename($tmp_path, $filepath)) { + die_error("Renaming temporary file $tmp_path to $filepath failed"); + } + } + echo UPLOAD_URL . $filename . "\n"; + send_notification("[hostfil.es] $_SERVER[REMOTE_ADDR] uploaded $_SERVER[PATH_INFO] $c_t -> $filename"); +} + + +function send_notification($note) { + $sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + socket_connect($sock, gethostbyname(NOTIFICATION_ADDRESS), NOTIFICATION_PORT); + socket_write($sock, $note, strlen($note)); + socket_close($sock); +} + + +function die_error($s) { + global $tmp_path, $filepath, $size; + header('Status: 500 Server Is 💩'); + echo $s; + error_log($s); + @$debug = json_encode(array('s'=>$_SERVER, 'f'=>$_FILES, 't'=>$tmp_path, 'p'=>$filepath, 'size' => $size)); + send_notification("[hostfil.es] $_SERVER[REMOTE_ADDR] ! $s\n$debug"); + exit; +} + + +function get_extension($file) { + $ext = strrchr($file, '.'); + if (FALSE === $ext) { + return ''; + } + return $ext; +} |
