Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e50e6d0
🧹 Cleanup instance architecture
bebiksior Jul 9, 2026
2cd1231
⚡️ Optimize instance data paths
bebiksior Jul 10, 2026
1f2946d
🔒 Harden access and interaction handling
bebiksior Jul 10, 2026
4fe4d18
✨ Show total logs per instance
bebiksior Jul 10, 2026
155d87a
Update README.md
bebiksior Jul 10, 2026
af3300b
✨ Update instance log counts live
bebiksior Jul 10, 2026
97e467b
🔔 Debounce Discord webhook notifications
bebiksior Jul 23, 2026
2c2f021
🛡️ Harden instance reliability
bebiksior Jul 23, 2026
eca3f93
🔀 Merge main into lt-cleanup
bebiksior Jul 23, 2026
e1ad33f
🐛 Fix log streaming and webhook batching
bebiksior Jul 28, 2026
588f6d3
Persist instance detail UI settings
bebiksior Aug 8, 2026
38f527e
✨ Add MCP instance publishing
bebiksior Aug 15, 2026
ca9f726
✨ Support MCP 2026-07-28
bebiksior Aug 15, 2026
9638ab2
⬆️ Bump default instance limit to 300
bebiksior Aug 15, 2026
db9d3cd
✨ Refresh assistant models
bebiksior Aug 15, 2026
639f081
🔐 Replace guest credentials with JWTs
bebiksior Aug 15, 2026
23257d3
✨ Add configurable AI providers
bebiksior Aug 15, 2026
8497e81
💄 Simplify AI settings UI
bebiksior Aug 15, 2026
bd0bd2a
💄 Remove AI settings clutter
bebiksior Aug 15, 2026
03b4fcf
💄 Polish pointer and mobile interactions
bebiksior Aug 15, 2026
f779bc6
✨ Refine AI controls
bebiksior Aug 15, 2026
2fe23dd
💄 Simplify AI controls
bebiksior Aug 15, 2026
d94bfb8
💄 Separate model controls
bebiksior Aug 15, 2026
79f08ea
💄 Remove selector chevrons
bebiksior Aug 15, 2026
025d359
🐛 Fix builder panel controls
bebiksior Aug 15, 2026
74a5451
✨ Autosave builder and virtualize instances
bebiksior Aug 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,10 @@ README.md
LICENSE
dist
.DS_Store
.env
.env.*
!.env.example
.httpworkbench
data
packages/*/node_modules
packages/*/dist
18 changes: 17 additions & 1 deletion Caddyfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
{
admin off
auto_https disable_redirects
servers {
# Only Cloudflare's published egress ranges may supply the client IP.
# Direct requests ignore CF-Connecting-IP and use the socket peer instead.
trusted_proxies static 173.245.48.0/20 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 141.101.64.0/18 108.162.192.0/18 190.93.240.0/20 188.114.96.0/20 197.234.240.0/22 198.41.128.0/17 162.158.0.0/15 104.16.0.0/13 104.24.0.0/14 172.64.0.0/13 131.0.72.0/22 2400:cb00::/32 2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 2a06:98c0::/29 2c0f:f248::/32
trusted_proxies_strict
client_ip_headers CF-Connecting-IP
}
log {
level ERROR
}
Expand All @@ -13,10 +20,18 @@ http://{$DOMAIN}, https://{$DOMAIN} {
}

encode gzip zstd
header {
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
X-Frame-Options "DENY"
Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()"
}
header /assets/* Cache-Control "public, max-age=31536000, immutable"

handle /api/* {
reverse_proxy backend:8081 {
header_up X-Internal-Real-IP {client_ip}
header_up -CF-Connecting-IP
header_up -X-Forwarded-For
header_up -X-Forwarded-Host
header_up -X-Forwarded-Proto
Expand Down Expand Up @@ -57,7 +72,8 @@ http://*.{$INSTANCES_DOMAIN}, https://*.{$INSTANCES_DOMAIN} {
}

reverse_proxy backend:8082 {
header_up X-Internal-Real-IP {remote_host}
header_up X-Internal-Real-IP {client_ip}
header_up -CF-Connecting-IP
header_up -X-Forwarded-For
header_up -X-Forwarded-Host
header_up -X-Forwarded-Proto
Expand Down
68 changes: 65 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,11 @@ curl https://httpworkbench.com/api/instances \
The full OpenAPI specification is served at `/api/openapi.json`

```bash
# Create a static instance
# Create an instance
curl -X POST https://httpworkbench.com/api/instances \
-H "Authorization: Bearer hwb_<id>_<secret>" \
-H "Content-Type: application/json" \
-d '{"kind":"static","raw":"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nhello"}'
-d '{"raw":"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nhello"}'

# Read its logs (paginate with nextCursor)
curl "https://httpworkbench.com/api/instances/<id>/logs?limit=50" \
Expand Down Expand Up @@ -143,6 +143,15 @@ Cloudflare proxy settings:
- `ns2.yourdomain.com`: `DNS only`
- `NS` records are just `NS` records; there is no proxy toggle for them

Caddy trusts `CF-Connecting-IP` only when the TCP connection comes from one of
Cloudflare's [published proxy ranges](https://www.cloudflare.com/ips/). A
request sent directly to the origin uses its socket address instead, even if it
supplies a spoofed `CF-Connecting-IP` or forwarding header. The ranges are kept
in `Caddyfile`; compare them with Cloudflare's current
[IPv4](https://www.cloudflare.com/ips-v4/) and
[IPv6](https://www.cloudflare.com/ips-v6/) lists when upgrading a self-hosted
deployment.

The wizard can start the stack for you, or you can run the compose command yourself at the end. The first startup may take a few minutes to build images and provision SSL certificates.

### Interaction Hostnames
Expand All @@ -168,6 +177,11 @@ docker compose -f docker-compose.yml -f docker-compose.dns.yml up -d --build

The main app certificate still uses Cloudflare DNS challenge. Instance subdomains under the delegated interaction zone are issued directly with on-demand TLS by Caddy the first time they are requested, stored in Caddy's persistent data volume, and renewed automatically.

Because the delegated interaction zone is `DNS only`, HTTP interactions reach
Caddy directly and are logged and rate-limited by the connection's source
address. Do not proxy this zone through Cloudflare: doing so changes protocol
behavior and makes every interaction originate from a Cloudflare proxy.

### Email (SMTP) interaction logging

HTTP Workbench can also receive email sent to any instance hostname and record it as an `SMTP` interaction alongside HTTP and DNS hits. A message sent to `anything@abc.instances.yourdomain.com` is logged against instance `abc`. The server only accepts and logs mail — it never relays or delivers it anywhere.
Expand All @@ -189,6 +203,52 @@ docker compose -f docker-compose.yml -f docker-compose.dns.yml -f docker-compose

No extra DNS records are required. Sending mail servers fall back to a host's `A` record when it has no `MX` record (RFC 5321 "implicit MX"), and instance hostnames already resolve to your server. The only requirements are that instance `A` records point at the real origin IP (a Cloudflare-proxied/orange-cloud record will not work for SMTP) and that public TCP port `25` reaches the backend. The server accepts plaintext on port 25 and does not advertise STARTTLS, so senders deliver over plaintext.

### Database migration backups

Before applying new migrations to an existing database, the backend creates a
consistent SQLite snapshot in the persistent data volume:

```text
${DATA_DIR}/db.sqlite.pre-migration-<appliedCount>-to-<availableCount>.backup
```

The deterministic file is retained and reused if startup is retried. No backup
is created for a fresh database or when every migration is already applied.

If an upgrade fails, stop the stack and list the available snapshots (do not
use `docker compose down -v`, which deletes the data volume):

```bash
docker compose down
docker compose run --rm --no-deps --entrypoint sh backend \
-c 'ls -1 /app/data/*.backup'
```

Then substitute the selected filename below. This preserves the failed database
for diagnosis, restores the snapshot, and removes SQLite sidecars that belong to
the failed database:

```bash
docker compose run --rm --no-deps --entrypoint sh backend -ec '
backup=/app/data/REPLACE_WITH_BACKUP_FILENAME
failed="/app/data/db.sqlite.failed-$(date -u +%Y%m%dT%H%M%SZ)"
cp /app/data/db.sqlite "$failed"
test ! -f /app/data/db.sqlite-wal || cp /app/data/db.sqlite-wal "$failed-wal"
test ! -f /app/data/db.sqlite-shm || cp /app/data/db.sqlite-shm "$failed-shm"
cp "$backup" /app/data/db.sqlite
rm -f /app/data/db.sqlite-wal /app/data/db.sqlite-shm
'
```

Finally, check out the previous application version and rebuild the stack before
starting it again. A database restored to a pre-migration snapshot must not be
started with the failed/new application version.

```bash
git checkout PREVIOUS_GIT_REF
docker compose up -d --build
```

## Your Data

HTTP Workbench stores just the basics in a local SQLite database: your Google ID for login (not the email), when you created your account, your PoC pages, webhooks, API keys, and the HTTP/DNS interaction logs (including IPs, headers, DNS questions, and request details). This is all needed for the app to work properly.
Expand All @@ -197,7 +257,9 @@ Full transparency: I never tried to access any data of users on ssrf.cvssadvisor

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.
Contributions are welcome! Please feel free to submit a Pull Request or a Issue.

Thanks a lot to [Sim4n6](https://github.com/Sim4n6) for multiple feature ideas!

## License

Expand Down
Loading
Loading