Compare commits
2 Commits
dd6a544d01
...
c4c62333fc
| Author | SHA1 | Date | |
|---|---|---|---|
| c4c62333fc | |||
| b03f5c5e11 |
@@ -1,7 +1,3 @@
|
||||
.git/
|
||||
files/
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
.gitignore
|
||||
README.md
|
||||
config.toml
|
||||
.git/
|
||||
*.db
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,2 @@
|
||||
files/
|
||||
*.db
|
||||
hardfiles
|
||||
16
Dockerfile
16
Dockerfile
@@ -1,12 +1,16 @@
|
||||
FROM golang:1.21-alpine as builder
|
||||
FROM golang:1.22-alpine AS builder
|
||||
WORKDIR /build
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY *.go ./
|
||||
RUN go build -o hardfiles main.go
|
||||
COPY main.go .
|
||||
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o hardfiles main.go
|
||||
|
||||
FROM golang:1.21-alpine as app
|
||||
FROM alpine:3.19
|
||||
RUN apk add --no-cache ca-certificates
|
||||
WORKDIR /app
|
||||
COPY --from=builder /build/hardfiles .
|
||||
RUN mkdir files
|
||||
CMD ["./hardfiles"]
|
||||
COPY www/ ./www/
|
||||
COPY config.toml .
|
||||
RUN mkdir -p files backgrounds
|
||||
EXPOSE 5000
|
||||
CMD ["./hardfiles"]
|
||||
|
||||
13
LICENSE
13
LICENSE
@@ -1,13 +0,0 @@
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
Version 2, December 2004
|
||||
|
||||
Copyright (C) 2023 SuperNETs <admin@supernets.org>
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim or modified
|
||||
copies of this license document, and changing it is allowed as long
|
||||
as the name is changed.
|
||||
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. You just DO WHAT THE FUCK YOU WANT TO.
|
||||
221
README.md
221
README.md
@@ -1,155 +1,148 @@
|
||||
# HARDFILES
|
||||
In today's digital landscape, the majority of image and file-sharing platforms are overburdened with bloatware, inundated with trackers, and riddled with restrictive usage limits. Moreover, they often cram unnecessary features, leaving users longing for a straightforward and secure file-sharing experience...
|
||||
|
||||
We designed HardFiles with a singular vision: to simplify and secure the process of file sharing. No fluff, no unnecessary features — just a streamlined, user-centric platform. What's more, we believe in transparency and community involvement, which is why HardFiles is open-source. Explore our service and contribute to its development at [https://hardfiles.org](https://hardfiles.org) now!
|
||||
No logs. No tracking. No analytics. No weird anime girls on the homepage.
|
||||
|
||||
🚫 **No JavaScript required to upload files!** 🚫
|
||||
Upload a file, get a link, it's shredded in 24 hours.
|
||||
|
||||
🛑 **No logs 📜, no tracking 👣, & no analytics!** 📊🚫
|
||||
## Features
|
||||
|
||||
🚷 **No weird anime girls or cringe weeb stuff on the homepage** 📵🚫
|
||||
- Drag-and-drop, clipboard paste, file picker, and curl uploads
|
||||
- Optional password protection on files
|
||||
- 7-pass secure file shredding on expiry
|
||||
- Random GIF backgrounds from a configurable directory
|
||||
- IP-based rate limiting
|
||||
- Streaming uploads (no memory buffering — handles files up to 5GB)
|
||||
- Single Go binary, no database
|
||||
|
||||
🔒 **All uploads are shredded securely ✂️🔥 after 24 hours** ⏳🗑️
|
||||
## Quick Start
|
||||
|
||||
## Terms of Service
|
||||
This platform serves as a public file hosting service. It is not actively monitored or overseen for specific content. Users are solely responsible for the content they upload and share. The administrator and owner of this server explicitly disclaim any responsibility for the content hosted and shared by users. Furthermore, the administrator is not liable for any damages, losses, or repercussions, either direct or indirect, resulting from the use of this service or the content found therein. Users are urged to use this service responsibly and ethically.
|
||||
|
||||
HardFiles is built on the principle of flexibility. If you choose to run your own instance of our service, you have the autonomy to define your own set of rules tailored to your community or organizational needs. However, when using our official service at [hardfiles.org](https://hardfiles.org), we maintain a minimalistic approach to rules. Our singular, non-negotiable rule is a strict prohibition against child pornography. We are committed to creating a safe environment for all users, and we have zero tolerance for any content that exploits the vulnerable.
|
||||
|
||||
## Deployment Guide for HardFiles
|
||||
|
||||
### 1. Clone this repository
|
||||
|
||||
This is necessary even when using the Docker image as the image does not contain the HardFiles frontend.
|
||||
|
||||
```shell
|
||||
git clone https://git.supernets.org/supernets/hardfiles.git
|
||||
```
|
||||
|
||||
### 2. Configuration:
|
||||
Start by adjusting the necessary configuration variables in `config.toml`.
|
||||
|
||||
### 3. Build and Run
|
||||
|
||||
#### Bare Metal:
|
||||
|
||||
Execute the following commands to build and initiate HardFiles:
|
||||
```shell
|
||||
```bash
|
||||
go build -o hardfiles main.go
|
||||
./hardfiles
|
||||
```
|
||||
|
||||
#### Docker Compose:
|
||||
Or with Docker:
|
||||
|
||||
Execute the following commands to build and initiate HardFiles in Docker:
|
||||
```shell
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### 3. Web Server Configuration:
|
||||
## Configuration
|
||||
|
||||
By default, HardFiles listens on port `5000`. For production environments, it's recommended to use a robust web server like Nginx or Caddy to proxy traffic to this port.
|
||||
Edit `config.toml`:
|
||||
|
||||
For obtaining the Let's Encrypt certificates, you can use tools like `certbot` that automatically handle the certification process for you. If you elect to use Caddy, in most circumstances it is able to handle certificates for you using Let's Encrypt.
|
||||
|
||||
Remember, by using a reverse proxy, you can run HardFiles without needing root privileges and maintain a more secure environment.
|
||||
|
||||
#### Using Nginx as a Reverse Proxy:
|
||||
|
||||
A reverse proxy takes requests from the Internet and forwards them to servers in an internal network. By doing so, it ensures that the actual application (in this case, HardFiles) doesn't need to run with root privileges or directly face the Internet, which is a security best practice.
|
||||
|
||||
Here's a basic setup for Nginx:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your_domain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:5000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
listen 443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem;
|
||||
}
|
||||
```toml
|
||||
webroot = "www"
|
||||
lport = "5000"
|
||||
vhost = "hardfiles.org"
|
||||
filelen = 6
|
||||
folder = "files"
|
||||
bgfolder = "backgrounds"
|
||||
max_upload_mb = 5120
|
||||
ttl_hours = 24
|
||||
rate_limit_per_min = 30
|
||||
```
|
||||
|
||||
Replace `your_domain.com` with your actual domain name. Save this configuration to a file, say `hardfiles.conf`, inside the `/etc/nginx/sites-available/` directory, and then create a symbolic link to `/etc/nginx/sites-enabled/`. Restart Nginx after this setup.
|
||||
## Usage
|
||||
|
||||
#### Using Caddy as a Reverse Proxy:
|
||||
### Browser
|
||||
Visit the site, drop a file, get a link.
|
||||
|
||||
Append the following to the Caddyfile, replacing your_domain.com with your chosen domain.
|
||||
### curl
|
||||
```bash
|
||||
# Upload a file
|
||||
curl -F file=@photo.png https://hardfiles.org/
|
||||
|
||||
```caddy
|
||||
your_domain.com {
|
||||
reverse_proxy localhost:5000
|
||||
}
|
||||
```
|
||||
|
||||
## cURL Uploads
|
||||
|
||||
You can upload files using cURL like so:
|
||||
|
||||
```shell
|
||||
curl -F file=@$1 https://hardfiles.org/
|
||||
```
|
||||
|
||||
Additionally, you can specify the amount of time before your upload is removed from the server. Currently the file expiry time must be provided in seconds and is limited to 5 days maximum. The following example will return a file that expires in 48 hours rather than the default of 24 hours.
|
||||
|
||||
```shell
|
||||
curl -F file=@$1 -F expiry=172800 https://hardfiles.org/
|
||||
# Upload with password
|
||||
curl -F file=@secret.pdf -F password=hunter2 https://hardfiles.org/
|
||||
```
|
||||
|
||||
### Bash Alias
|
||||
|
||||
If you frequently upload files to HardFiles via the command line, you can streamline the process by setting up a bash alias. This allows you to use a simple command, like `upload`, to push your files to HardFiles using `curl`.
|
||||
|
||||
#### Setting Up:
|
||||
|
||||
1. **Edit your `.bashrc` file:** Open your `~/.bashrc` file in a text editor. You can use `nano` or `vim` for this purpose:
|
||||
```shell
|
||||
nano ~/.bashrc
|
||||
```
|
||||
|
||||
2. **Add the `upload` function:** At the end of the `.bashrc` file, append the following function (replace the domain if you are running your own instance):
|
||||
```shell
|
||||
```bash
|
||||
# Add to ~/.bashrc
|
||||
upload() {
|
||||
curl -F file=@$1 https://hardfiles.org/
|
||||
}
|
||||
```
|
||||
|
||||
3. Reload your .bashrc file: To make the new function available in your current session, reload your .bashrc:
|
||||
```shell
|
||||
source ~/.bashrc
|
||||
Then just `upload /path/to/file.jpg`.
|
||||
|
||||
## Backgrounds
|
||||
|
||||
Add .gif files to the `backgrounds/` directory. Each page load picks one at random. The more chaotic, the better.
|
||||
|
||||
## Nginx Reverse Proxy
|
||||
|
||||
Hardfiles is designed to run behind nginx. Key configuration for large file uploads:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name hardfiles.org;
|
||||
|
||||
# CRITICAL: Must match or exceed max_upload_mb in config.toml
|
||||
client_max_body_size 5120m;
|
||||
|
||||
# Disable request buffering — stream directly to backend
|
||||
proxy_request_buffering off;
|
||||
|
||||
# Increase timeouts for large uploads
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 60s;
|
||||
|
||||
# Increase header buffer for large multipart boundaries
|
||||
proxy_buffer_size 16k;
|
||||
proxy_buffers 4 32k;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_http_version 1.1;
|
||||
}
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/hardfiles.org/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/hardfiles.org/privkey.pem;
|
||||
}
|
||||
```
|
||||
|
||||
#### Usage:
|
||||
Now, you can easily upload files to HardFiles using the upload command followed by the path to your file. For example:
|
||||
**Important nginx settings:**
|
||||
- `client_max_body_size 5120m` — must match `max_upload_mb` in config.toml, or nginx will reject uploads before they reach hardfiles
|
||||
- `proxy_request_buffering off` — prevents nginx from buffering the upload to disk before forwarding (critical for memory-limited servers)
|
||||
- `proxy_read_timeout 3600s` — 1 hour timeout for large uploads on slow connections
|
||||
|
||||
```shell
|
||||
upload /path/to/your/file.jpg
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
# Build and run
|
||||
docker compose up -d
|
||||
|
||||
# Volumes mounted:
|
||||
# ./files:/app/files — uploaded files (auto-cleared every 24h)
|
||||
# ./backgrounds:/app/backgrounds — GIF backgrounds
|
||||
# ./config.toml:/app/config.toml — configuration
|
||||
```
|
||||
|
||||
This will upload the specified file to HardFiles and return a direct link to the file.
|
||||
## Security
|
||||
|
||||
## Roadmap
|
||||
- Idea - Uploads stored on a remotely mounted drive or S3 compatible volume, isolating them from the actual service server. Multiple mirrored instances behind a round robin reading from the same remote mount for scaling.
|
||||
- Random wallpapers as an optional extra, kept simple without javascript. Maybe a local shell script that modifies the index.html on a timer.
|
||||
- Fix index wallpaper alignment on smartphones.
|
||||
- Clean up CSS.
|
||||
- Warrant Canary
|
||||
- Footer or some link to SupernNETs & this repository & terms of service txt.
|
||||
- Tor & i2p support services *(This can quite possibly be a very bad idea to operate. Maybe a captcha for .onion/.i2p uploads only...)*
|
||||
- Files are shredded with 7-pass random overwrite before deletion (effective on HDD; ceremonial on SSD — use dm-crypt/LUKS for SSD)
|
||||
- Path traversal prevention on all routes
|
||||
- Upload size limits enforced at the HTTP level
|
||||
- Password-protected files use bcrypt hashing
|
||||
- MIME type allowlist for inline serving (images, PDFs, text, audio, video) — HTML/SVG/JS forced to download to prevent stored XSS
|
||||
- Content-Security-Policy headers on all HTML responses
|
||||
- No file listing or directory browsing
|
||||
|
||||
## Credits
|
||||
- 🚀 **delorean**, our Senior Director of IRC Diplomacy & SuperNets Brand Strategy 🌐 for developing hardfiles.
|
||||
- 🤝 **hgw**, our Principal Designer of Digital Aquariums & Rare Fish Showcases 🐠 for branding the product.
|
||||
- 💼 **acidvegas**, our Global Director of IRC Communications 💬 for funding the project 💰.
|
||||
|
||||
___
|
||||
- **delorean** for developing hardfiles
|
||||
- **hgw** for branding the product
|
||||
- **acidvegas** for funding the project
|
||||
|
||||
###### Mirrors
|
||||
[acid.vegas](https://git.acid.vegas/hardfiles) • [GitHub](https://github.com/supernets/hardfiles) • [GitLab](https://gitlab.com/supernets/hardfiles) • [SuperNETs](https://git.supernets.org/supernets/hardfiles)
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
BIN
backgrounds/gun-revolver.gif
Normal file
BIN
backgrounds/gun-revolver.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 542 KiB |
@@ -1,8 +1,9 @@
|
||||
webroot = "www"
|
||||
lport = "5000"
|
||||
vhost = "hardfiles.org"
|
||||
dbfile = "dbfile.db"
|
||||
filelen = 6
|
||||
folder = "files"
|
||||
default_ttl = 86400
|
||||
maximum_ttl = 604800
|
||||
bgfolder = "backgrounds"
|
||||
max_upload_mb = 5120
|
||||
ttl_hours = 24
|
||||
rate_limit_per_min = 30
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
services:
|
||||
hardfiles:
|
||||
container_name: hardfiles
|
||||
image: git.supernets.org/supernets/hardfiles:latest
|
||||
build: .
|
||||
volumes:
|
||||
- "$PWD/files:/app/files"
|
||||
- "$PWD/www:/app/www"
|
||||
- "$PWD/config.toml:/app/config.toml"
|
||||
- ./files:/app/files
|
||||
- ./backgrounds:/app/backgrounds
|
||||
- ./config.toml:/app/config.toml
|
||||
ports:
|
||||
- "5000:5000"
|
||||
- "5000:5000"
|
||||
restart: unless-stopped
|
||||
|
||||
21
go.mod
21
go.mod
@@ -1,20 +1,9 @@
|
||||
module hardfiles
|
||||
module github.com/realhardfiles/hardfiles
|
||||
|
||||
go 1.21.0
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.3.2
|
||||
github.com/gabriel-vasile/mimetype v1.4.3
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/landlock-lsm/go-landlock v0.0.0-20230607164353-b03374193cb2
|
||||
github.com/rs/zerolog v1.31.0
|
||||
go.etcd.io/bbolt v1.3.8
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
kernel.org/pub/linux/libs/security/libcap/psx v1.2.66 // indirect
|
||||
github.com/BurntSushi/toml v1.6.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.13
|
||||
golang.org/x/crypto v0.49.0
|
||||
)
|
||||
|
||||
44
go.sum
44
go.sum
@@ -1,38 +1,6 @@
|
||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/landlock-lsm/go-landlock v0.0.0-20230607164353-b03374193cb2 h1:urvYVFXXrgiZWCYCQ6LWEE98QdU4Mvd5zwXuTllWMTg=
|
||||
github.com/landlock-lsm/go-landlock v0.0.0-20230607164353-b03374193cb2/go.mod h1:D25+lEYNcoxH7SgM/VOvWNrF3ZNAfRdydDuVbQBe6yE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
|
||||
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
kernel.org/pub/linux/libs/security/libcap/psx v1.2.66 h1:ikIhPzfkSSAEwBOU+2DWhoF+xnGUhvlMTfQjBVhvzQY=
|
||||
kernel.org/pub/linux/libs/security/libcap/psx v1.2.66/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
|
||||
567
main_test.go
Normal file
567
main_test.go
Normal file
@@ -0,0 +1,567 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// testConf sets up a test config using temp directories.
|
||||
func testConf(t *testing.T) (filesDir, bgDir, wwwDir string) {
|
||||
t.Helper()
|
||||
filesDir = t.TempDir()
|
||||
bgDir = t.TempDir()
|
||||
wwwDir = t.TempDir()
|
||||
|
||||
conf = Config{
|
||||
Webroot: wwwDir,
|
||||
Lport: "0",
|
||||
Vhost: "test.local",
|
||||
Filelen: 6,
|
||||
Folder: filesDir,
|
||||
Bgfolder: bgDir,
|
||||
MaxUploadMB: 10,
|
||||
TTLHours: 24,
|
||||
RateLimitPerMin: 100,
|
||||
}
|
||||
rateLimiter = NewRateLimiter(conf.RateLimitPerMin)
|
||||
return
|
||||
}
|
||||
|
||||
// buildUploadRequest creates a multipart/form-data POST with an optional password.
|
||||
func buildUploadRequest(t *testing.T, filename, content, password string) *http.Request {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
mw := multipart.NewWriter(&buf)
|
||||
|
||||
if password != "" {
|
||||
_ = mw.WriteField("password", password)
|
||||
}
|
||||
fw, err := mw.CreateFormFile("file", filename)
|
||||
if err != nil {
|
||||
t.Fatalf("create form file: %v", err)
|
||||
}
|
||||
if _, err := io.WriteString(fw, content); err != nil {
|
||||
t.Fatalf("write content: %v", err)
|
||||
}
|
||||
mw.Close()
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/", &buf)
|
||||
req.Header.Set("Content-Type", mw.FormDataContentType())
|
||||
return req
|
||||
}
|
||||
|
||||
// ---- Upload tests ----------------------------------------------------------
|
||||
|
||||
func TestUploadHappyPath(t *testing.T) {
|
||||
testConf(t)
|
||||
handler := newMux()
|
||||
|
||||
req := buildUploadRequest(t, "hello.txt", "hello world", "")
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
body := strings.TrimSpace(rr.Body.String())
|
||||
if !strings.HasPrefix(body, "https://test.local/f/") {
|
||||
t.Fatalf("unexpected response body: %q", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadWithPassword(t *testing.T) {
|
||||
filesDir, _, _ := testConf(t)
|
||||
handler := newMux()
|
||||
|
||||
req := buildUploadRequest(t, "secret.txt", "top secret", "hunter2")
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", rr.Code)
|
||||
}
|
||||
body := strings.TrimSpace(rr.Body.String())
|
||||
fileID := strings.TrimPrefix(body, "https://test.local/f/")
|
||||
|
||||
// Meta sidecar must exist.
|
||||
metaPath := filepath.Join(filesDir, fileID+".meta")
|
||||
if _, err := os.Stat(metaPath); err != nil {
|
||||
t.Fatalf("expected .meta sidecar at %s: %v", metaPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadExceedsSizeLimit(t *testing.T) {
|
||||
testConf(t)
|
||||
conf.MaxUploadMB = 1 // 1 MB limit for this test
|
||||
rateLimiter = NewRateLimiter(conf.RateLimitPerMin)
|
||||
handler := newMux()
|
||||
|
||||
// Build a body larger than 1 MB
|
||||
bigContent := strings.Repeat("x", 2*1024*1024)
|
||||
req := buildUploadRequest(t, "big.bin", bigContent, "")
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusRequestEntityTooLarge {
|
||||
t.Fatalf("expected 413, got %d: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadNoFile(t *testing.T) {
|
||||
testConf(t)
|
||||
handler := newMux()
|
||||
|
||||
// POST with only a password field, no file
|
||||
var buf bytes.Buffer
|
||||
mw := multipart.NewWriter(&buf)
|
||||
_ = mw.WriteField("password", "nope")
|
||||
mw.Close()
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/", &buf)
|
||||
req.Header.Set("Content-Type", mw.FormDataContentType())
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadMIMEDetection(t *testing.T) {
|
||||
testConf(t)
|
||||
handler := newMux()
|
||||
|
||||
// PNG magic bytes
|
||||
pngHeader := "\x89PNG\r\n\x1a\n" + strings.Repeat("\x00", 100)
|
||||
req := buildUploadRequest(t, "image.png", pngHeader, "")
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
body := strings.TrimSpace(rr.Body.String())
|
||||
fileID := strings.TrimPrefix(body, "https://test.local/f/")
|
||||
if !strings.HasSuffix(fileID, ".png") {
|
||||
t.Fatalf("expected .png extension, got %q", fileID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadTempFileCleanedOnError(t *testing.T) {
|
||||
testConf(t)
|
||||
// Make the files dir read-only so CreateTemp succeeds but Rename fails.
|
||||
// Instead, verify no temp files linger after a bad request (no file field).
|
||||
handler := newMux()
|
||||
|
||||
var buf bytes.Buffer
|
||||
mw := multipart.NewWriter(&buf)
|
||||
mw.Close()
|
||||
req := httptest.NewRequest(http.MethodPost, "/", &buf)
|
||||
req.Header.Set("Content-Type", mw.FormDataContentType())
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// Any response is fine; verify no .upload-* temp files remain.
|
||||
entries, _ := os.ReadDir(conf.Folder)
|
||||
for _, e := range entries {
|
||||
if strings.HasPrefix(e.Name(), ".upload-") {
|
||||
t.Errorf("leftover temp file: %s", e.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Download tests --------------------------------------------------------
|
||||
|
||||
func writeTestFile(t *testing.T, filesDir, name, content string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(filesDir, name)
|
||||
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("write test file: %v", err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func TestDownloadHappyPath(t *testing.T) {
|
||||
filesDir, _, _ := testConf(t)
|
||||
handler := newMux()
|
||||
|
||||
writeTestFile(t, filesDir, "abc123.txt", "file contents")
|
||||
req := httptest.NewRequest(http.MethodGet, "/f/abc123.txt", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
if !strings.Contains(rr.Body.String(), "file contents") {
|
||||
t.Fatalf("body missing file contents: %q", rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownload404(t *testing.T) {
|
||||
testConf(t)
|
||||
handler := newMux()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/f/doesnotexist.txt", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusNotFound {
|
||||
t.Fatalf("expected 404, got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadPasswordProtectedGETPrompt(t *testing.T) {
|
||||
filesDir, _, _ := testConf(t)
|
||||
handler := newMux()
|
||||
|
||||
writeTestFile(t, filesDir, "locked.txt", "secret")
|
||||
hash, _ := bcrypt.GenerateFromPassword([]byte("pass123"), bcrypt.MinCost)
|
||||
meta := &FileMeta{PasswordHash: string(hash)}
|
||||
_ = writeMeta(filepath.Join(filesDir, "locked.txt.meta"), meta)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/f/locked.txt", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 (password prompt), got %d", rr.Code)
|
||||
}
|
||||
if !strings.Contains(rr.Body.String(), "PASSWORD") {
|
||||
t.Fatalf("expected password prompt in body")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadPasswordCorrect(t *testing.T) {
|
||||
filesDir, _, _ := testConf(t)
|
||||
handler := newMux()
|
||||
|
||||
writeTestFile(t, filesDir, "locked2.txt", "secret content")
|
||||
hash, _ := bcrypt.GenerateFromPassword([]byte("correct"), bcrypt.MinCost)
|
||||
meta := &FileMeta{PasswordHash: string(hash)}
|
||||
_ = writeMeta(filepath.Join(filesDir, "locked2.txt.meta"), meta)
|
||||
|
||||
body := strings.NewReader("password=correct")
|
||||
req := httptest.NewRequest(http.MethodPost, "/f/locked2.txt", body)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
if !strings.Contains(rr.Body.String(), "secret content") {
|
||||
t.Fatalf("expected file content in response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadPasswordWrong(t *testing.T) {
|
||||
filesDir, _, _ := testConf(t)
|
||||
handler := newMux()
|
||||
|
||||
writeTestFile(t, filesDir, "locked3.txt", "secret content")
|
||||
hash, _ := bcrypt.GenerateFromPassword([]byte("correct"), bcrypt.MinCost)
|
||||
meta := &FileMeta{PasswordHash: string(hash)}
|
||||
_ = writeMeta(filepath.Join(filesDir, "locked3.txt.meta"), meta)
|
||||
|
||||
body := strings.NewReader("password=wrong")
|
||||
req := httptest.NewRequest(http.MethodPost, "/f/locked3.txt", body)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expected 401, got %d", rr.Code)
|
||||
}
|
||||
if !strings.Contains(rr.Body.String(), "WRONG PASSWORD") {
|
||||
t.Fatalf("expected WRONG PASSWORD in body")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Path traversal tests --------------------------------------------------
|
||||
|
||||
func TestPathTraversalFile(t *testing.T) {
|
||||
testConf(t)
|
||||
handler := newMux()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/f/../etc/passwd", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// Should either be 400 or 404 — not 200
|
||||
if rr.Code == http.StatusOK {
|
||||
t.Fatalf("path traversal succeeded: got 200")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathTraversalBg(t *testing.T) {
|
||||
testConf(t)
|
||||
handler := newMux()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/bg/../etc/passwd", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code == http.StatusOK {
|
||||
t.Fatalf("path traversal on /bg/ succeeded: got 200")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathTraversalStatic(t *testing.T) {
|
||||
testConf(t)
|
||||
handler := newMux()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/static/../etc/passwd", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code == http.StatusOK {
|
||||
t.Fatalf("path traversal on /static/ succeeded: got 200")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Corrupt meta ----------------------------------------------------------
|
||||
|
||||
func TestCorruptMetaFailOpen(t *testing.T) {
|
||||
filesDir, _, _ := testConf(t)
|
||||
handler := newMux()
|
||||
|
||||
writeTestFile(t, filesDir, "corrupt.txt", "open content")
|
||||
// Write invalid JSON to the meta sidecar.
|
||||
if err := os.WriteFile(filepath.Join(filesDir, "corrupt.txt.meta"), []byte("{bad json"), 0644); err != nil {
|
||||
t.Fatalf("write corrupt meta: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/f/corrupt.txt", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// Should serve file (fail open), not error.
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 (fail open), got %d: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Index page ------------------------------------------------------------
|
||||
|
||||
func TestIndexPage(t *testing.T) {
|
||||
testConf(t)
|
||||
handler := newMux()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", rr.Code)
|
||||
}
|
||||
if !strings.Contains(rr.Body.String(), "HARDFILES") {
|
||||
t.Fatalf("index page missing HARDFILES")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexWithBackgrounds(t *testing.T) {
|
||||
_, bgDir, _ := testConf(t)
|
||||
handler := newMux()
|
||||
|
||||
// Create a fake gif in the bg dir.
|
||||
gifPath := filepath.Join(bgDir, "test.gif")
|
||||
if err := os.WriteFile(gifPath, []byte("GIF89a"), 0644); err != nil {
|
||||
t.Fatalf("write gif: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", rr.Code)
|
||||
}
|
||||
if !strings.Contains(rr.Body.String(), "/bg/test.gif") {
|
||||
t.Fatalf("expected bg URL in index body")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexEmptyBackgrounds(t *testing.T) {
|
||||
testConf(t)
|
||||
handler := newMux()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 with empty bg dir, got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- NameGen ---------------------------------------------------------------
|
||||
|
||||
func TestNameGen(t *testing.T) {
|
||||
const length = 8
|
||||
ambiguous := "0OIl1"
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
id, err := genID(length)
|
||||
if err != nil {
|
||||
t.Fatalf("genID error: %v", err)
|
||||
}
|
||||
if len(id) != length {
|
||||
t.Errorf("expected length %d, got %d: %q", length, len(id), id)
|
||||
}
|
||||
for _, c := range ambiguous {
|
||||
if strings.ContainsRune(id, c) {
|
||||
t.Errorf("id %q contains ambiguous char %c", id, c)
|
||||
}
|
||||
}
|
||||
seen[id] = true
|
||||
}
|
||||
// With a 54-char alphabet and length 8, collision in 100 draws is astronomically unlikely.
|
||||
if len(seen) < 90 {
|
||||
t.Errorf("too many collisions: only %d unique IDs in 100 draws", len(seen))
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Shred -----------------------------------------------------------------
|
||||
|
||||
func TestShred(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "shred_me.txt")
|
||||
original := "sensitive data here"
|
||||
if err := os.WriteFile(path, []byte(original), 0644); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
|
||||
if err := shredFile(path); err != nil {
|
||||
t.Fatalf("shredFile: %v", err)
|
||||
}
|
||||
|
||||
// File should be gone after shredding.
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Fatalf("file still exists after shred")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Rate limiter ----------------------------------------------------------
|
||||
|
||||
func TestRateLimiterAllowsUnderLimit(t *testing.T) {
|
||||
rl := NewRateLimiter(5)
|
||||
for i := 0; i < 5; i++ {
|
||||
if !rl.Allow("1.2.3.4") {
|
||||
t.Fatalf("request %d should be allowed", i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimiterBlocksOverLimit(t *testing.T) {
|
||||
rl := NewRateLimiter(3)
|
||||
for i := 0; i < 3; i++ {
|
||||
rl.Allow("1.2.3.4")
|
||||
}
|
||||
if rl.Allow("1.2.3.4") {
|
||||
t.Fatal("4th request should be blocked")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimiterDifferentIPs(t *testing.T) {
|
||||
rl := NewRateLimiter(2)
|
||||
for i := 0; i < 2; i++ {
|
||||
rl.Allow("10.0.0.1")
|
||||
}
|
||||
// Different IP should still be allowed.
|
||||
if !rl.Allow("10.0.0.2") {
|
||||
t.Fatal("different IP should not be rate limited")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimiterResetsAfterWindow(t *testing.T) {
|
||||
rl := NewRateLimiter(2)
|
||||
rl.Allow("5.5.5.5")
|
||||
rl.Allow("5.5.5.5")
|
||||
// Manually age the timestamps so they fall outside the window.
|
||||
rl.mu.Lock()
|
||||
rec := rl.records["5.5.5.5"]
|
||||
rl.mu.Unlock()
|
||||
rec.mu.Lock()
|
||||
for i := range rec.timestamps {
|
||||
rec.timestamps[i] = time.Now().Add(-2 * time.Minute)
|
||||
}
|
||||
rec.mu.Unlock()
|
||||
|
||||
if !rl.Allow("5.5.5.5") {
|
||||
t.Fatal("should be allowed after window resets")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Upload returns URL that can be downloaded -----------------------------
|
||||
|
||||
func TestUploadThenDownload(t *testing.T) {
|
||||
testConf(t)
|
||||
handler := newMux()
|
||||
|
||||
// Upload
|
||||
req := buildUploadRequest(t, "round.txt", "round trip content", "")
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("upload: expected 200, got %d", rr.Code)
|
||||
}
|
||||
uploadedURL := strings.TrimSpace(rr.Body.String())
|
||||
fileID := strings.TrimPrefix(uploadedURL, "https://test.local/f/")
|
||||
|
||||
// Download
|
||||
req2 := httptest.NewRequest(http.MethodGet, "/f/"+fileID, nil)
|
||||
rr2 := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr2, req2)
|
||||
if rr2.Code != http.StatusOK {
|
||||
t.Fatalf("download: expected 200, got %d", rr2.Code)
|
||||
}
|
||||
if !strings.Contains(rr2.Body.String(), "round trip content") {
|
||||
t.Fatalf("downloaded content mismatch: %q", rr2.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Integration: rate limit via HTTP handler ------------------------------
|
||||
|
||||
func TestUploadRateLimitedViaHandler(t *testing.T) {
|
||||
testConf(t)
|
||||
conf.RateLimitPerMin = 2
|
||||
rateLimiter = NewRateLimiter(conf.RateLimitPerMin)
|
||||
handler := newMux()
|
||||
|
||||
makeUpload := func() int {
|
||||
var buf bytes.Buffer
|
||||
mw := multipart.NewWriter(&buf)
|
||||
fw, _ := mw.CreateFormFile("file", "f.txt")
|
||||
fmt.Fprint(fw, "data")
|
||||
mw.Close()
|
||||
req := httptest.NewRequest(http.MethodPost, "/", &buf)
|
||||
req.Header.Set("Content-Type", mw.FormDataContentType())
|
||||
req.RemoteAddr = "9.9.9.9:1234"
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
return rr.Code
|
||||
}
|
||||
|
||||
if c := makeUpload(); c != http.StatusOK {
|
||||
t.Fatalf("1st upload: expected 200, got %d", c)
|
||||
}
|
||||
if c := makeUpload(); c != http.StatusOK {
|
||||
t.Fatalf("2nd upload: expected 200, got %d", c)
|
||||
}
|
||||
if c := makeUpload(); c != http.StatusTooManyRequests {
|
||||
t.Fatalf("3rd upload: expected 429, got %d", c)
|
||||
}
|
||||
}
|
||||
760
www/index.html
760
www/index.html
@@ -1,276 +1,502 @@
|
||||
<!DOCTYPE html>
|
||||
<html style="overflow: hidden;">
|
||||
<head>
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pako/1.0.4/pako_deflate.min.js"></script>
|
||||
<script type="text/javascript" src="//code.jquery.com/jquery-1.10.2.min.js"></script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="fist.ico">
|
||||
<title>HARDFILES</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
background-image: url('https://media.tenor.com/fYnd0R6F-0UAAAAC/gun-revolver.gif');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
overflow: hidden;
|
||||
background: black;
|
||||
background-image: url('https://media.tenor.com/fYnd0R6F-0UAAAAC/gun-revolver.gif');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
width: 750px;
|
||||
}
|
||||
.hf {
|
||||
font-size: 3rem;
|
||||
}
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>HARDFILES</title>
|
||||
<link rel="icon" href="/static/fist.ico">
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'VT323';
|
||||
src: url('/static/vt323.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
}
|
||||
:root {
|
||||
--bg: #000000;
|
||||
--overlay: rgba(0,0,0,0.45);
|
||||
--red: #FF0000;
|
||||
--cyan: #00FFFF;
|
||||
--green: #00FF00;
|
||||
--yellow: #FFFF00;
|
||||
--text: #FFFFFF;
|
||||
--text-dim: #888888;
|
||||
--font: 'VT323', monospace;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--font);
|
||||
font-size: 20px;
|
||||
}
|
||||
#bg {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-image: url('{{.BgURL}}');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
z-index: 0;
|
||||
}
|
||||
#bg.loaded { opacity: 1; }
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: var(--overlay);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
body::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
transparent 0px,
|
||||
transparent 2px,
|
||||
rgba(0,0,0,0.12) 2px,
|
||||
rgba(0,0,0,0.12) 3px
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
.wrap {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.hflogo {
|
||||
width: 50% !important;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.container {
|
||||
width: 970px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.container {
|
||||
width: 1170px;
|
||||
}
|
||||
.hf {
|
||||
font-size: 5rem;
|
||||
}
|
||||
}
|
||||
input,select {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.file-upload{
|
||||
display:block;
|
||||
text-align:center;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 12px;
|
||||
width: 300px;
|
||||
}
|
||||
.file-upload .file-select{
|
||||
display:block;
|
||||
border: 2px solid #dce4ec;
|
||||
color: black;
|
||||
cursor:pointer;
|
||||
height:40px;
|
||||
line-height:40px;
|
||||
text-align:left;
|
||||
background:#FFFFFF;
|
||||
overflow:hidden;
|
||||
position:relative;
|
||||
}
|
||||
.file-upload .file-select .file-select-button{
|
||||
background:#dce4ec;
|
||||
padding:0 10px;
|
||||
display:inline-block;
|
||||
height:40px;
|
||||
line-height:40px;
|
||||
}
|
||||
.file-upload .file-select .file-select-name{
|
||||
line-height:40px;
|
||||
display:inline-block;
|
||||
padding:0 10px;
|
||||
}
|
||||
.file-upload .file-select:hover{
|
||||
border-color:red;
|
||||
transition:all .2s ease-in-out;
|
||||
-moz-transition:all .2s ease-in-out;
|
||||
-webkit-transition:all .2s ease-in-out;
|
||||
-o-transition:all .2s ease-in-out;
|
||||
}
|
||||
.file-upload .file-select:hover .file-select-button{
|
||||
background:red;
|
||||
color:#FFFFFF;
|
||||
transition:all .2s ease-in-out;
|
||||
-moz-transition:all .2s ease-in-out;
|
||||
-webkit-transition:all .2s ease-in-out;
|
||||
-o-transition:all .2s ease-in-out;
|
||||
}
|
||||
.file-upload.active .file-select{
|
||||
border-color:red;
|
||||
transition:all .2s ease-in-out;
|
||||
-moz-transition:all .2s ease-in-out;
|
||||
-webkit-transition:all .2s ease-in-out;
|
||||
-o-transition:all .2s ease-in-out;
|
||||
}
|
||||
.file-upload.active .file-select .file-select-button{
|
||||
background:red;
|
||||
color:#FFFFFF;
|
||||
transition:all .2s ease-in-out;
|
||||
-moz-transition:all .2s ease-in-out;
|
||||
-webkit-transition:all .2s ease-in-out;
|
||||
-o-transition:all .2s ease-in-out;
|
||||
}
|
||||
.file-upload .file-select input[type=file]{
|
||||
z-index:100;
|
||||
cursor:pointer;
|
||||
position:absolute;
|
||||
height:100%;
|
||||
width:100%;
|
||||
top:0;
|
||||
left:0;
|
||||
opacity:0;
|
||||
filter:alpha(opacity=0);
|
||||
}
|
||||
.file-upload .file-select.file-select-disabled{
|
||||
opacity:0.65;
|
||||
}
|
||||
.file-upload .file-select.file-select-disabled:hover{
|
||||
cursor:default;
|
||||
display:block;
|
||||
border: 2px solid #dce4ec;
|
||||
color: red;
|
||||
cursor:pointer;
|
||||
height:40px;
|
||||
line-height:40px;
|
||||
margin-top:5px;
|
||||
text-align:left;
|
||||
background:#FFFFFF;
|
||||
overflow:hidden;
|
||||
position:relative;
|
||||
}
|
||||
.file-upload .file-select.file-select-disabled:hover .file-select-button{
|
||||
background:#dce4ec;
|
||||
color:#666666;
|
||||
padding:0 10px;
|
||||
display:inline-block;
|
||||
height:40px;
|
||||
line-height:40px;
|
||||
}
|
||||
.file-upload .file-select.file-select-disabled:hover .file-select-name{
|
||||
line-height:40px;
|
||||
display:inline-block;
|
||||
padding:0 10px;
|
||||
}
|
||||
.subform {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
button {
|
||||
display: inline-block;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
margin: 0;
|
||||
padding: 15px;
|
||||
height: 44px;
|
||||
color: #fff;
|
||||
font: 19px/15px 'Oswald', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial, "Lucida Grande", sans-serif;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
-webkit-border-radius: 3px;
|
||||
-moz-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
-webkit-box-sizing: border-box;
|
||||
/* Safari/Chrome, other WebKit */
|
||||
-moz-box-sizing: border-box;
|
||||
/* Firefox, other Gecko */
|
||||
box-sizing: border-box;
|
||||
/* Opera/IE 8+ */
|
||||
-webkit-font-smoothing:antialiased;
|
||||
-webkit-text-size-adjust:none;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,.15);
|
||||
}
|
||||
button:hover {
|
||||
background: #c93c1d;
|
||||
-webkit-transition:all .25s ease-in-out;
|
||||
-moz-transition:all .25s ease-in-out;
|
||||
-o-transition:all .25s ease-in-out;
|
||||
transition:all .25s ease-in-out;
|
||||
}
|
||||
button:active {
|
||||
background: #ae3318;
|
||||
}
|
||||
button.light {
|
||||
background: #fff;
|
||||
color: #555759;
|
||||
}
|
||||
button.light:hover {
|
||||
background: red;
|
||||
color: #fff;
|
||||
}
|
||||
.hflogo {
|
||||
width: 90%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img src="header.png" class="hflogo" alt="supernets" style="margin-top: 10rem;">
|
||||
<h2 style="font-size: 1rem; color: #ffffff; font-weight: 200;">curl -F file=@example.png https://hardfiles.org/</h1>
|
||||
<form method="POST" class="subform" enctype="multipart/form-data">
|
||||
<div class="file-upload" style="display: flex; justify-content: center; width: 25rem;">
|
||||
<div class="file-select" style="width: 100%;">
|
||||
<div class="file-select-button" id="fileName">Browse</div>
|
||||
<div class="file-select-name" id="noFile" style="cursor: pointer;">No file chosen...</div>
|
||||
<input type="file" name="file" id="chooseFile">
|
||||
</div>
|
||||
<button class="light" type="submit" style="margin-left: 0.7rem;">up</button>
|
||||
</div>
|
||||
</form>
|
||||
<p style="color: white; font-weight: 300;">⚠️ Uploads are erased after 24 hours</p>
|
||||
</div>
|
||||
<div style="position: relative;">
|
||||
<a href="https://supernets.org/" target="_blank" style="position: absolute; bottom: 10px; right:10px; display: flex; align-items: center; text-decoration: none;">
|
||||
<p style="font-weight:200; color:#ffffff">A SUPERNETS</p>
|
||||
<img src="https://git.supernets.org/assets/img/logo.png" width="30px" style="margin: 0 5px;">
|
||||
<p style="font-weight:200; color: #ffffff">SERVICE</p>
|
||||
</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
/* ---- Logo glitch ---- */
|
||||
.logo-wrap {
|
||||
position: relative;
|
||||
width: 50%;
|
||||
max-width: 600px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
@media (max-width: 768px) { .logo-wrap { width: 90%; } }
|
||||
.logo-wrap img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.logo-wrap::before,
|
||||
.logo-wrap::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url('/static/header.png');
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
z-index: 0;
|
||||
}
|
||||
.logo-wrap::before {
|
||||
animation: glitch-before 3s infinite steps(1);
|
||||
}
|
||||
.logo-wrap::after {
|
||||
animation: glitch-after 3s infinite steps(1);
|
||||
animation-delay: 0.15s;
|
||||
}
|
||||
@keyframes glitch-before {
|
||||
0%,4% { opacity: 0; }
|
||||
5% { opacity: 0.8; transform: translate(-5px, 2px); filter: hue-rotate(90deg);
|
||||
clip-path: polygon(0 15%, 100% 15%, 100% 35%, 0 35%); }
|
||||
7% { opacity: 0.8; transform: translate(5px, -2px); filter: hue-rotate(180deg);
|
||||
clip-path: polygon(0 55%, 100% 55%, 100% 75%, 0 75%); }
|
||||
8%,29% { opacity: 0; }
|
||||
30% { opacity: 0.7; transform: translate(-3px, -1px); filter: hue-rotate(270deg);
|
||||
clip-path: polygon(0 40%, 100% 40%, 100% 60%, 0 60%); }
|
||||
32%,59% { opacity: 0; }
|
||||
60% { opacity: 0.9; transform: translate(6px, 1px); filter: hue-rotate(120deg);
|
||||
clip-path: polygon(0 10%, 100% 10%, 100% 25%, 0 25%); }
|
||||
61% { opacity: 0.9; transform: translate(-4px, 3px); filter: hue-rotate(200deg);
|
||||
clip-path: polygon(0 70%, 100% 70%, 100% 90%, 0 90%); }
|
||||
63%,84% { opacity: 0; }
|
||||
85% { opacity: 0.7; transform: translate(3px, -2px); filter: hue-rotate(45deg);
|
||||
clip-path: polygon(0 30%, 100% 30%, 100% 50%, 0 50%); }
|
||||
87%,100%{ opacity: 0; }
|
||||
}
|
||||
@keyframes glitch-after {
|
||||
0%,14% { opacity: 0; }
|
||||
15% { opacity: 0.7; transform: translate(4px, 2px); filter: hue-rotate(300deg);
|
||||
clip-path: polygon(0 20%, 100% 20%, 100% 45%, 0 45%); }
|
||||
17%,44% { opacity: 0; }
|
||||
45% { opacity: 0.8; transform: translate(-6px, -1px); filter: hue-rotate(60deg);
|
||||
clip-path: polygon(0 60%, 100% 60%, 100% 80%, 0 80%); }
|
||||
47% { opacity: 0.6; transform: translate(3px, 3px); filter: hue-rotate(150deg);
|
||||
clip-path: polygon(0 5%, 100% 5%, 100% 20%, 0 20%); }
|
||||
49%,74% { opacity: 0; }
|
||||
75% { opacity: 0.9; transform: translate(-5px, 2px); filter: hue-rotate(240deg);
|
||||
clip-path: polygon(0 45%, 100% 45%, 100% 65%, 0 65%); }
|
||||
77%,100%{ opacity: 0; }
|
||||
}
|
||||
|
||||
/* ---- Tagline ---- */
|
||||
.tagline {
|
||||
color: var(--red);
|
||||
font-size: 1.4rem;
|
||||
letter-spacing: 0.15em;
|
||||
margin-bottom: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ---- Drop zone ---- */
|
||||
.drop-zone {
|
||||
position: relative;
|
||||
width: 440px;
|
||||
max-width: 90vw;
|
||||
min-height: 150px;
|
||||
border: 2px dashed var(--red);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
font-family: var(--font);
|
||||
text-align: center;
|
||||
animation: pulse-opacity 2s ease-in-out infinite;
|
||||
transition: background 0.2s, border-style 0.1s;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.drop-zone { width: 90vw; min-height: 180px; }
|
||||
}
|
||||
@keyframes pulse-opacity {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.55; }
|
||||
}
|
||||
.drop-zone:hover, .drop-zone.drag-over {
|
||||
animation: shake 0.3s ease;
|
||||
opacity: 1;
|
||||
border-style: solid;
|
||||
background: rgba(255,0,0,0.07);
|
||||
}
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
20% { transform: translateX(-4px); }
|
||||
40% { transform: translateX(4px); }
|
||||
60% { transform: translateX(-3px); }
|
||||
80% { transform: translateX(3px); }
|
||||
}
|
||||
.drop-zone.uploading {
|
||||
animation: pulse-opacity 0.5s ease-in-out infinite;
|
||||
border-style: dashed;
|
||||
border-color: var(--red);
|
||||
}
|
||||
.drop-zone.success {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
border-style: solid;
|
||||
border-color: var(--green);
|
||||
}
|
||||
.drop-zone.error-state {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
border-style: solid;
|
||||
border-color: var(--red);
|
||||
}
|
||||
.drop-main {
|
||||
font-size: 2rem;
|
||||
letter-spacing: 0.1em;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.drop-sub {
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.drop-url {
|
||||
font-size: 1.6rem;
|
||||
color: var(--red);
|
||||
word-break: break-all;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.drop-url:hover { text-decoration: underline; }
|
||||
.drop-another {
|
||||
font-size: 1rem;
|
||||
color: var(--text-dim);
|
||||
cursor: pointer;
|
||||
margin-top: 0.5rem;
|
||||
text-decoration: underline;
|
||||
background: none;
|
||||
border: none;
|
||||
font-family: var(--font);
|
||||
min-height: 44px;
|
||||
}
|
||||
.drop-another:hover { color: var(--text); }
|
||||
#file-input { display: none; }
|
||||
|
||||
/* ---- Password controls ---- */
|
||||
#pw-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.3rem;
|
||||
font-size: 1.2rem;
|
||||
color: var(--text-dim);
|
||||
min-height: 44px;
|
||||
}
|
||||
#pw-row input[type=checkbox] { width: 18px; height: 18px; cursor: pointer; }
|
||||
#pw-input-wrap { margin-top: 0.5rem; }
|
||||
#pw-input {
|
||||
background: #000;
|
||||
border: 2px dashed var(--red);
|
||||
color: var(--text);
|
||||
font-family: var(--font);
|
||||
font-size: 1.3rem;
|
||||
padding: 0.5rem 0.8rem;
|
||||
width: 440px;
|
||||
max-width: 90vw;
|
||||
outline: none;
|
||||
min-height: 44px;
|
||||
}
|
||||
#pw-input:focus { border-style: solid; }
|
||||
|
||||
/* ---- Curl example ---- */
|
||||
.curl-box {
|
||||
margin-top: 0.5rem;
|
||||
width: 440px;
|
||||
max-width: 90vw;
|
||||
border-left: 3px solid var(--green);
|
||||
padding: 0.75rem 1rem;
|
||||
background: rgba(0,255,0,0.04);
|
||||
}
|
||||
.curl-box code {
|
||||
color: var(--green);
|
||||
font-family: var(--font);
|
||||
font-size: 1.1rem;
|
||||
text-shadow: 0 0 8px rgba(0,255,0,0.5);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ---- Warning ---- */
|
||||
.warning {
|
||||
margin-top: 0.5rem;
|
||||
color: var(--yellow);
|
||||
font-size: 1.3rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-align: center;
|
||||
animation: flicker 4s infinite;
|
||||
}
|
||||
@keyframes flicker {
|
||||
0%,95%,100% { opacity: 1; }
|
||||
96% { opacity: 0.3; }
|
||||
97% { opacity: 1; }
|
||||
98% { opacity: 0.5; }
|
||||
99% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* ---- Live region ---- */
|
||||
#live-region { position: absolute; left: -9999px; top: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="bg"></div>
|
||||
<div class="wrap">
|
||||
<div class="logo-wrap">
|
||||
<img src="/static/header.png" alt="HARDFILES">
|
||||
</div>
|
||||
<div class="tagline">ephemeral • volatile • gone in 24h</div>
|
||||
|
||||
<button class="drop-zone" id="drop-zone" aria-label="Upload file — click, drag, or paste">
|
||||
<input type="file" id="file-input" aria-hidden="true" tabindex="-1">
|
||||
<div class="drop-main" id="drop-main">DROP FILE HERE</div>
|
||||
<div class="drop-sub" id="drop-sub">click to browse • ctrl+v to paste • drag & drop</div>
|
||||
</button>
|
||||
|
||||
<div aria-live="polite" id="live-region"></div>
|
||||
|
||||
<div id="pw-row">
|
||||
<input type="checkbox" id="pw-check" aria-label="Password protect this file">
|
||||
<label for="pw-check">password protect</label>
|
||||
</div>
|
||||
<div id="pw-input-wrap" style="display:none">
|
||||
<label for="pw-input" style="position:absolute;left:-9999px">File password</label>
|
||||
<input type="password" id="pw-input" placeholder="set password" autocomplete="new-password" aria-label="File password">
|
||||
</div>
|
||||
|
||||
<div class="curl-box">
|
||||
<code>curl -F file=@example.png https://hardfiles.org/</code>
|
||||
</div>
|
||||
|
||||
<div class="warning">ALL UPLOADS ARE SHREDDED AFTER 24 HOURS</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('#chooseFile').bind('change', function (e) {
|
||||
var file = e.target.files[0];
|
||||
if (!file) {
|
||||
$(".file-upload").removeClass('active');
|
||||
$("#noFile").text("No file chosen...");
|
||||
return;
|
||||
}
|
||||
(function() {
|
||||
// Background fade-in
|
||||
var bgEl = document.getElementById('bg');
|
||||
var bgStyle = bgEl.style.backgroundImage;
|
||||
if (bgStyle && bgStyle !== 'url("")' && bgStyle !== "url('')") {
|
||||
var url = bgStyle.replace(/url\(["']?([^"')]+)["']?\)/, '$1');
|
||||
var img = new Image();
|
||||
img.onload = function() { bgEl.classList.add('loaded'); };
|
||||
img.onerror = function() { bgEl.classList.add('loaded'); };
|
||||
img.src = url;
|
||||
} else {
|
||||
bgEl.classList.add('loaded');
|
||||
}
|
||||
|
||||
$(".file-upload").addClass('active');
|
||||
$("#noFile").text(file.name.replace("C:\\fakepath\\", ""));
|
||||
var dropZone = document.getElementById('drop-zone');
|
||||
var fileInput = document.getElementById('file-input');
|
||||
var dropMain = document.getElementById('drop-main');
|
||||
var dropSub = document.getElementById('drop-sub');
|
||||
var liveRegion = document.getElementById('live-region');
|
||||
var pwCheck = document.getElementById('pw-check');
|
||||
var pwInputWrap = document.getElementById('pw-input-wrap');
|
||||
var pwInput = document.getElementById('pw-input');
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
var binaryData = event.target.result;
|
||||
// Compress the file data
|
||||
var compressed = pako.gzip(new Uint8Array(binaryData), { level:1});
|
||||
var compressedFile = new Blob([compressed], { type: "application/gzip" });
|
||||
updateFileInput(compressedFile, file.name);
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
// Password toggle
|
||||
pwCheck.addEventListener('change', function() {
|
||||
pwInputWrap.style.display = pwCheck.checked ? 'block' : 'none';
|
||||
if (pwCheck.checked) pwInput.focus();
|
||||
});
|
||||
|
||||
// Click to open file picker
|
||||
dropZone.addEventListener('click', function(e) {
|
||||
if (e.target.closest('#pw-row') || e.target.closest('#pw-input-wrap')) return;
|
||||
if (dropZone.classList.contains('success')) return;
|
||||
fileInput.click();
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', function() {
|
||||
if (fileInput.files && fileInput.files[0]) {
|
||||
uploadFile(fileInput.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// Drag and drop
|
||||
dropZone.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('drag-over');
|
||||
});
|
||||
dropZone.addEventListener('dragleave', function(e) {
|
||||
if (!dropZone.contains(e.relatedTarget)) {
|
||||
dropZone.classList.remove('drag-over');
|
||||
}
|
||||
});
|
||||
dropZone.addEventListener('drop', function(e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('drag-over');
|
||||
var files = e.dataTransfer && e.dataTransfer.files;
|
||||
if (files && files[0]) uploadFile(files[0]);
|
||||
});
|
||||
|
||||
// Paste
|
||||
document.addEventListener('paste', function(e) {
|
||||
var items = e.clipboardData && e.clipboardData.items;
|
||||
if (!items) return;
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
if (items[i].kind === 'file') {
|
||||
var file = items[i].getAsFile();
|
||||
if (file) { uploadFile(file); break; }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function setUploading(pct) {
|
||||
dropZone.classList.remove('success', 'error-state', 'drag-over');
|
||||
dropZone.classList.add('uploading');
|
||||
var txt = (pct !== null && pct !== undefined) ? 'UPLOADING... ' + pct + '%' : 'UPLOADING...';
|
||||
dropMain.textContent = txt;
|
||||
dropSub.textContent = '';
|
||||
liveRegion.textContent = txt;
|
||||
}
|
||||
|
||||
function setSuccess(url) {
|
||||
dropZone.classList.remove('uploading', 'error-state', 'drag-over');
|
||||
dropZone.classList.add('success');
|
||||
|
||||
// Clear children
|
||||
while (dropMain.firstChild) dropMain.removeChild(dropMain.firstChild);
|
||||
while (dropSub.firstChild) dropSub.removeChild(dropSub.firstChild);
|
||||
|
||||
var urlEl = document.createElement('div');
|
||||
urlEl.className = 'drop-url';
|
||||
urlEl.textContent = url;
|
||||
urlEl.title = 'Click to copy';
|
||||
urlEl.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(url).then(function() {
|
||||
urlEl.textContent = 'COPIED';
|
||||
setTimeout(function() { urlEl.textContent = url; }, 1200);
|
||||
});
|
||||
|
||||
function updateFileInput(compressedFile, fileName) {
|
||||
var dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(new File([compressedFile], fileName + '.5000'));
|
||||
$('#chooseFile')[0].files = dataTransfer.files;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var anotherBtn = document.createElement('button');
|
||||
anotherBtn.className = 'drop-another';
|
||||
anotherBtn.textContent = 'upload another';
|
||||
anotherBtn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
resetZone();
|
||||
});
|
||||
|
||||
dropMain.appendChild(urlEl);
|
||||
dropSub.appendChild(anotherBtn);
|
||||
liveRegion.textContent = 'Upload complete. URL: ' + url;
|
||||
}
|
||||
|
||||
function setError(msg) {
|
||||
dropZone.classList.remove('uploading', 'success', 'drag-over');
|
||||
dropZone.classList.add('error-state');
|
||||
dropMain.textContent = msg || 'UPLOAD FAILED';
|
||||
dropSub.textContent = '';
|
||||
liveRegion.textContent = msg || 'Upload failed';
|
||||
setTimeout(resetZone, 3000);
|
||||
}
|
||||
|
||||
function resetZone() {
|
||||
dropZone.classList.remove('uploading', 'success', 'error-state', 'drag-over');
|
||||
dropMain.textContent = 'DROP FILE HERE';
|
||||
dropSub.textContent = 'click to browse \u2022 ctrl+v to paste \u2022 drag \u0026 drop';
|
||||
liveRegion.textContent = '';
|
||||
fileInput.value = '';
|
||||
}
|
||||
|
||||
function uploadFile(file) {
|
||||
setUploading(0);
|
||||
var fd = new FormData();
|
||||
fd.append('file', file);
|
||||
if (pwCheck.checked && pwInput.value) {
|
||||
fd.append('password', pwInput.value);
|
||||
}
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/');
|
||||
xhr.upload.onprogress = function(e) {
|
||||
if (e.lengthComputable) {
|
||||
var pct = Math.round(e.loaded / e.total * 100);
|
||||
setUploading(pct);
|
||||
}
|
||||
};
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
setSuccess(xhr.responseText.trim());
|
||||
} else if (xhr.status === 413) {
|
||||
setError('FILE TOO LARGE');
|
||||
} else {
|
||||
setError('UPLOAD FAILED: ' + xhr.status);
|
||||
}
|
||||
};
|
||||
xhr.onerror = function() { setError('NETWORK ERROR'); };
|
||||
xhr.send(fd);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
BIN
www/pressstart2p.woff2
Normal file
BIN
www/pressstart2p.woff2
Normal file
Binary file not shown.
BIN
www/vt323.woff2
Normal file
BIN
www/vt323.woff2
Normal file
Binary file not shown.
Reference in New Issue
Block a user