Mirror any website on 127.0.0.1, with one command.
localportal -u https://example.com
Open http://127.0.0.1:3000 and the browser shows example.com, with working links, logins, forms, images, video and websockets. The site is served through your machine, as if it were running locally.
It does the same job as an nginx proxy_pass block, without installing nginx or writing any config. It also handles the details a plain proxy gets wrong, such as redirects, cookies, absolute links and HTTPS-only headers, so you stay on the mirror instead of being sent back to the real site.
Install
pip install localportal-cli
This installs the localportal command and the localportal Python module.
To install the latest version straight from GitHub:
pip install git+https://github.com/Ademking/localportal
Quick start
# Mirror a site on http://127.0.0.1:3000
localportal -u https://example.com
# Choose a port and open the browser automatically
localportal -u https://news.ycombinator.com -p 8080 --open
# The scheme is optional (https is assumed)
localportal python.org
Press Ctrl+C to stop. Every request is logged as it happens:
localportal is mirroring https://www.python.org
Open http://127.0.0.1:3000/
Press Ctrl+C to stop.
02:23:42 GET / -> 200 (330 ms)
02:23:42 GET /static/stylesheets/mq.css -> 200 (310 ms)
02:23:42 GET /static/img/python-logo.png -> 200 (313 ms)
What it's good for
- Local development against a real site. Work on a frontend or browser extension while a staging or production site appears to run on localhost.
- Testing and automation. Point Playwright, Selenium, Lighthouse or your scripts at a stable local address.
- Debugging. Every request passes through one place you control, so you can see exactly what the browser asks for.
- Sharing on your network. Run with
--bind 0.0.0.0and other devices on your LAN can open the mirror. - Demos. Show a site on a local address, with extra headers (such as an auth token) added to every request.
Options
| Option | Description |
|---|---|
-u, --url URL |
Website to mirror. The scheme is optional. |
-p, --port PORT |
Local port. Default: 3000. |
-b, --bind ADDR |
Address to listen on. Default: 127.0.0.1. Use 0.0.0.0 to share on your LAN. |
-H, --header 'NAME: VALUE' |
Extra header to send upstream. Repeatable. |
--no-rewrite |
Leave absolute links in response bodies untouched. |
--no-resolve |
Don't follow the startup www/https redirect. |
--forward-headers |
Send X-Forwarded-For, X-Real-IP and X-Forwarded-Proto upstream. |
-k, --insecure |
Don't verify the upstream TLS certificate (for self-signed staging servers). |
-o, --open |
Open the mirror in your browser. |
-q, --quiet |
Don't log each request. |
-V, --version |
Print the version. |
More examples:
# Send an auth header with every request
localportal -u https://staging.myapp.com -H "Authorization: Bearer <token>"
# Start on a specific page: http://127.0.0.1:3000/ redirects to /docs
localportal -u https://example.com/docs
# Mirror a dev server with a self-signed certificate
localportal -u https://192.168.1.20:8443 -k
# Same as the localportal command
python -m localportal -u https://example.com
How it works
browser localportal real site
http://127.0.0.1:3000 ──▶ rewrite Host, Origin, Referer ──▶ https://example.com
◀── rewrite links, cookies, headers ◀──
Each request is replayed against the real site with the site's own Host header and TLS SNI. Then the response is adjusted so the browser keeps treating the mirror as the site:
| Without localportal | What localportal does |
|---|---|
| Redirects send you to the real site | Location, Content-Location, Link and Refresh headers are rewritten to point at the mirror |
Absolute links (https://site/…, //site/…, https:\/\/site in JSON) leave the mirror |
They are rewritten in HTML, CSS, JS, JSON, XML and SVG |
Cookies with Domain=site and Secure are rejected on plain http |
Those attributes are removed so logins keep working (__Host- and __Secure- cookies keep Secure) |
HSTS, CSP upgrade-insecure-requests and Alt-Svc force HTTPS |
These headers are dropped, along with <meta> CSP tags and the SRI integrity attributes that rewriting would invalidate |
The server's CSRF checks reject the wrong Origin or Referer |
Both are rewritten back to the real site on the way out |
example.com redirects to www.example.com, creating a redirect loop |
The canonical host is detected once at startup |
| WebSockets don't pass through | They are proxied in both directions, with subprotocols |
| Large files get buffered | Video, downloads and Server-Sent Events are streamed, and Range requests work |
Compared with nginx
This nginx config:
server {
listen 3000;
location / {
proxy_pass https://example.com;
proxy_set_header Host example.com;
proxy_ssl_server_name on;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
is equivalent to:
localportal -u https://example.com
localportal also does everything in the table above, which nginx would need sub_filter, proxy_redirect, proxy_cookie_domain and WebSocket upgrade blocks to do.
Use it from Python
from localportal import LocalPortal
LocalPortal("https://example.com", port=3000).run()
All CLI options are available as keyword arguments:
LocalPortal(
"https://staging.myapp.com",
host="0.0.0.0",
port=8080,
extra_headers={"Authorization": "Bearer <token>"},
verify_ssl=False,
).run(open_browser=True)
To embed it in your own aiohttp server or tests, LocalPortal(...).make_app() returns the aiohttp.web.Application, and await LocalPortal(...).serve() runs it inside an existing event loop.
Limitations
- Only the main host is mirrored, plus its
www.version. Subdomains such ascdn.example.comandapi.example.com, and third-party hosts, load directly from the internet. That's fine for public assets, but cross-origin APIs that checkOriginmay refuse requests from the mirror. - URLs built at runtime by JavaScript (
"https://" + host) can't be rewritten ahead of time. - Third-party sign-in (Google, GitHub, etc.) usually fails, because the provider redirects back to the real domain.
- One origin per port. Every site you mirror on the same port shares cookies, storage and service workers. Use a different port for each site, or clear the site data when you switch.
- Bot protection (Cloudflare challenges, CAPTCHAs) sees ordinary traffic from your IP and may still challenge you.
Only mirror sites you're allowed to access, and respect their terms of service.
Development
git clone https://github.com/Ademking/localportal
cd localportal
pip install -e ".[dev]"
pytest
The tests start a fake upstream server and check HTML rewriting, redirects, cookies, request forwarding, binary streaming and WebSockets.
License
Metadata
Release files for localportal-cli 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| localportal_cli-0.1.0.tar.gz | 18.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| localportal_cli-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 33.2 kB
Release files / localportal_cli-0.1.0.tar.gz
| Download URL | localportal_cli-0.1.0.tar.gz |
|---|---|
| Size | 18.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
58396a446eeeb9b61cf38a82ac5269524596f5d21c391b77cb3dffd4f652c80a
|
|
BLAKE2b-256 checksum How to use checksums |
625ebee6e80e84f04f909cd5e6972d6d7b7d81e5cbb675f7b82b4d499f6b5c68
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.10
|
Release files / localportal_cli-0.1.0-py3-none-any.whl
| Download URL | localportal_cli-0.1.0-py3-none-any.whl |
|---|---|
| Size | 14.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
dbdd14a430c80da310083bd6746bab3c5532b0e96c156a378ad2d0031d4ebb9e
|
|
BLAKE2b-256 checksum How to use checksums |
300cf36a85e4be23a22002bbf51ab2c0fb84fad729a0da56c3809e5ada061d8d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.10
|