# sitedrop

Upload an HTML file. Get a URL. That's it.

Self-hosted static site hosting. Deploy single HTML pages to your own server with one command.

```bash
pip install sitedrop
sitedrop login
sitedrop upload portfolio.html
# => Live at https://yourserver.com/portfolio
```

## Features

- **One command deploy** — `sitedrop upload file.html` and it's live
- **Clean URLs** — `portfolio.html` becomes `/portfolio`
- **Self-hosted** — runs on your own VM, you own your data
- **Single package** — same `pip install` for both server and client
- **Simple auth** — password login with JWT tokens, managed by the CLI
- **Full control** — upload, update, delete, and list your sites
- **No build step** — upload raw HTML, no frameworks or config needed
- **Public viewing** — anyone can view deployed sites, only you can manage them

## Quick Start

### Server (on your VM)

```bash
pip install sitedrop
sitedrop setup
sitedrop start
```

### Client (on your laptop)

```bash
pip install sitedrop
sitedrop login
sitedrop upload portfolio.html
# => Live at https://yourserver.com/portfolio
```

Same package, different commands.

## CLI Reference

### Server Commands

Run these on your VM.

#### `sitedrop setup`

Interactive setup. Sets the admin password and configures the server bind address.

```bash
sitedrop setup
```

```
Set admin password: ********
Confirm password: ********
Bind host [127.0.0.1]:
Bind port [8000]:
Config saved to ~/.sitedrop/server.json
```

Use `--dev` to skip host/port prompts and default to `127.0.0.1:8000`:

```bash
sitedrop setup --dev
```

#### `sitedrop start`

Start the server.

```bash
sitedrop start
```

This starts the server as a background process. Use `--dev` to run in the foreground with auto-reload:

```bash
sitedrop start --dev
```

For direct HTTPS (without a reverse proxy), pass your certificate and key:

```bash
sitedrop start --ssl-certfile cert.pem --ssl-keyfile key.pem
```

SSL paths are saved in server config so `sitedrop restart` preserves them.

#### `sitedrop stop`

Stop the server. Sends SIGTERM for a graceful shutdown, falling back to SIGKILL after 5 seconds.

```bash
sitedrop stop
```

#### `sitedrop restart`

Restart the server (stop + start). Supports `--dev` and SSL options.

```bash
sitedrop restart
```

#### `sitedrop status`

Check if the server is running.

```bash
sitedrop status
```

### Client Commands

Run these from anywhere.

#### `sitedrop login`

Authenticate with your server. Saves credentials locally.

```bash
sitedrop login
# Server URL [http://localhost:8000]: https://yourserver.com
# Password: ********
# Logged in to https://yourserver.com
```

Credentials are saved to `~/.sitedroprc` (chmod 600) so you only need to do this once. The CLI automatically refreshes expired tokens using the saved password.

#### `sitedrop upload <file>`

Upload an HTML file. The site name is derived from the filename.

```bash
sitedrop upload portfolio.html        # => /portfolio
sitedrop upload my-blog.html          # => /my-blog
sitedrop upload index.html            # => /index
```

Override the name with `--name`:

```bash
sitedrop upload page.html --name about   # => /about
```

If a site with that name already exists, the CLI prompts for confirmation. Use `--force` to skip the prompt and overwrite:

```bash
sitedrop upload page.html --force
```

#### `sitedrop list`

List all deployed sites.

```bash
sitedrop list
```

```
Name                     Size  Modified
------------------------------------------------------------
portfolio                4200  2026-02-20T10:30:00+00:00
my-blog                 12100  2026-02-19T08:15:00+00:00
```

#### `sitedrop delete <name>`

Remove a deployed site. Prompts for confirmation before deleting. Use `--yes` / `-y` to skip the prompt.

```bash
sitedrop delete portfolio
# Delete portfolio? [y/N]: y
# Deleted portfolio

sitedrop delete portfolio --yes
# Deleted portfolio
```

#### `sitedrop info`

Show the configured server URL and token status (valid, expired, or not logged in).

```bash
sitedrop info
```

#### `sitedrop password`

Change the server password. Prompts for your current and new password, then updates saved credentials. All previously issued tokens are invalidated.

```bash
sitedrop password
```

## API Reference

The CLI is a wrapper around a REST API. You can also use it directly with `curl`.

All management endpoints require a JWT token in the `Authorization` header. Get a token by authenticating with your password.

### Authentication

#### `POST /api/auth`

Get a JWT token.

```bash
curl -X POST https://yourserver.com/api/auth \
  -H "Content-Type: application/json" \
  -d '{"password": "your-password"}'
```

```json
{
  "token": "eyJhbGciOiJIUzI1NiIs..."
}
```

Tokens expire after 24 hours.

#### `POST /api/password`

Change your password. Requires authentication.

```bash
curl -X POST https://yourserver.com/api/password \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"current_password": "current", "new_password": "new-one"}'
```

#### `GET /api/health`

Health check. No authentication required.

```bash
curl https://yourserver.com/api/health
```

```json
{
  "status": "ok"
}
```

### Sites

#### `PUT /api/sites/{name}`

Upload or update a site. Send the HTML as the request body.

```bash
curl -X PUT https://yourserver.com/api/sites/portfolio \
  -H "Authorization: Bearer <token>" \
  --data-binary @portfolio.html
```

To prevent accidental overwrites, send `If-None-Match: *` — the server returns `412 Precondition Failed` if the site already exists:

```bash
curl -X PUT https://yourserver.com/api/sites/portfolio \
  -H "Authorization: Bearer <token>" \
  -H "If-None-Match: *" \
  --data-binary @portfolio.html
```

```json
{
  "name": "portfolio",
  "size": 4200,
  "modified": "2026-02-20T10:30:00+00:00"
}
```

Site names must match `^[a-zA-Z0-9][a-zA-Z0-9_-]*$` and be at most 240 characters. Maximum upload size is 10 MB.

#### `GET /api/sites`

List all deployed sites. Requires authentication.

```bash
curl https://yourserver.com/api/sites \
  -H "Authorization: Bearer <token>"
```

```json
{
  "sites": [
    {
      "name": "portfolio",
      "size": 4200,
      "modified": "2026-02-20T10:30:00+00:00"
    }
  ]
}
```

#### `DELETE /api/sites/{name}`

Delete a site. Requires authentication.

```bash
curl -X DELETE https://yourserver.com/api/sites/portfolio \
  -H "Authorization: Bearer <token>"
```

### Viewing Sites

#### `GET /{name}`

View a deployed site. No authentication required.

```bash
curl https://yourserver.com/portfolio
```

Returns the HTML content directly. Non-existent sites return `404 Not Found` as plain text.

Served sites include `ETag` (SHA-256 of content) and `Cache-Control: no-cache` headers. Browsers cache the content but revalidate on each request — unchanged content returns `304 Not Modified`.

## Server Prerequisites

- Python 3.10+
- A VM (Google Cloud, AWS, DigitalOcean, etc.)
- A reverse proxy (nginx, caddy, etc.) for HTTPS and public access, or use `--ssl-certfile`/`--ssl-keyfile` for direct TLS

## Configuration

### Server

Server configuration is stored in `~/.sitedrop/server.json`:

```json
{
  "password_hash": "$2b$12$...",
  "jwt_secret": "auto-generated-on-first-run",
  "host": "127.0.0.1",
  "port": 8000,
  "sites_dir": "~/.sitedrop/sites",
  "ssl_certfile": null,
  "ssl_keyfile": null
}
```

### Client

Client configuration is stored in `~/.sitedroprc` (chmod 600):

```json
{
  "server_url": "https://yourserver.com",
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "password": "saved-for-auto-refresh"
}
```

The CLI automatically refreshes expired tokens using the saved password.

## Limits

- **File size:** 10 MB per upload
- **Site name:** 240 characters max, alphanumeric plus hyphens and underscores, must start with alphanumeric
- **File format:** Must be valid UTF-8 text
- **Auth rate limit:** 5 login attempts per minute per IP

## API Error Codes

| Code | Meaning |
|------|---------|
| 400  | Invalid site name or non-UTF-8 content |
| 401  | Wrong password or expired/invalid token |
| 404  | Site not found |
| 412  | Site already exists (when `If-None-Match: *` is sent) |
| 413  | File exceeds 10 MB limit |
| 429  | Too many login attempts |
| 500  | Server not configured (run `sitedrop setup`) |

## Security

- **Always use HTTPS in production.** Either use `--ssl-certfile`/`--ssl-keyfile` for direct TLS, or put a reverse proxy (nginx, caddy) in front of sitedrop.
- **Protect `~/.sitedroprc`.** The client stores your password in plaintext to enable automatic token refresh. The file is created with `chmod 600` (owner read/write only). Treat it like your SSH keys.
- **Uploaded HTML is served as-is.** Sitedrop does not sanitize HTML content. If you upload a page with malicious scripts, visitors will run them.
- **Passwords are hashed with bcrypt** on the server. The JWT secret is auto-generated (256-bit) on first setup and rotated on every password change, invalidating all existing tokens.
- **Login attempts are rate-limited** to 5 per minute per IP to protect against brute-force attacks.
- The server sets `X-Content-Type-Options: nosniff` and `X-Frame-Options: DENY` on all responses.
- **API responses include a `Content-Security-Policy` header** (`default-src 'none'; frame-ancestors 'none'`). This header is not applied to served sites so their content renders normally.

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `SITEDROP_CONFIG` | `~/.sitedrop/server.json` | Override server config file path |
| `SITEDROP_CLIENT_CONFIG` | `~/.sitedroprc` | Override client config file path |

Useful for Docker deployments or running multiple instances.

## How It Works

```
User                    CLI                     Server                  Visitor
 |                       |                       |                       |
 |-- sitedrop upload f.html-->|                    |                       |
 |                       |-- PUT /api/sites/f -->|                       |
 |                       |                       |-- save f.html to disk |
 |                       |<-- 200 OK ------------|                       |
 |<-- Live at /f --------|                       |                       |
 |                       |                       |                       |
 |                       |                       |<-- GET /f ------------|
 |                       |                       |-- serve f.html ------>|
```

The server is a single FastAPI application that handles both the management API and site serving:

- **API routes** (`/api/*`) require JWT authentication and handle CRUD operations
- **Site routes** (`/{name}`) are public and serve stored HTML files directly
- HTML files are stored on the filesystem at `~/.sitedrop/sites/{name}.html`
- Passwords are hashed with bcrypt, JWTs are signed with a server-generated secret

## Development

Run the server locally for development:

```bash
git clone https://github.com/williamtao345/sitedrop.git
cd sitedrop

python -m venv venv
source venv/bin/activate
pip install -e ".[dev]"

# Set up with a test password
sitedrop setup --dev

# Run the server with auto-reload
sitedrop start --dev
```

Test the CLI against your local server:

```bash
sitedrop login
# Server URL [http://localhost:8000]:
# Password: your-test-password

sitedrop upload test.html
# => Live at http://localhost:8000/test
```

Run tests:

```bash
pytest
```

## Troubleshooting

**"Cannot connect to server"** — The client can't reach the server. Check that the server is running (`sitedrop status`) and that the URL in `~/.sitedroprc` is correct.

**"Authentication failed"** — Wrong password or expired token. The CLI auto-refreshes tokens, but if your password changed, run `sitedrop login` again.

**"Too many login attempts"** — You've been rate-limited. Wait 60 seconds and try again.

**"File too large"** — Upload exceeds the 10 MB limit.

**"File must be valid UTF-8"** — You tried to upload a binary file. Sitedrop only hosts UTF-8 HTML.

**"Server failed to start"** — Usually means the port is already in use. Check with `lsof -i :8000` or change the port with `sitedrop setup`.

**Server appears started but isn't responding** — Check `sitedrop status`. If it says "not running (stale PID file)", the server crashed after starting. Check your reverse proxy logs or run `sitedrop start --dev` to see errors in the foreground.

## License

MIT
