Skip to content

Commit c4d318e

Browse files
committed
fix(format): apply ruff format; add /proxy/forward and /proxy/ip docs
- ruff-format proxy.py (frozenset literal spacing) - Add docs/usage/forward.md: full /proxy/forward reference including {mediaflow_ip} placeholder, safety controls, configuration, examples, and comparison table vs /proxy/stream - Update docs/usage/overview.md: add /proxy/forward to endpoint list - Update docs/configuration/environment.md: document PUBLIC_IP, FORWARD_ALLOWED_HOSTS, FORWARD_DENIED_HOSTS, and body size limits
1 parent 9af7ec8 commit c4d318e

4 files changed

Lines changed: 228 additions & 41 deletions

File tree

docs/configuration/environment.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,11 @@ Set the following environment variables:
2727
- `DASH_SEGMENT_CACHE_TTL`: Optional. TTL in seconds for cached DASH segments. Default: `60`. Longer values help with slow network playback.
2828
- `FORWARDED_ALLOW_IPS`: Optional. Controls which IP addresses are trusted to provide forwarded headers (X-Forwarded-For, X-Forwarded-Proto, etc.) when MediaFlow Proxy is deployed behind reverse proxies or load balancers. Default: `127.0.0.1`. See [Forwarded Headers Configuration](networking.md#forwarded-headers-configuration) for detailed usage.
2929

30+
### `/proxy/forward` settings
31+
32+
- `PUBLIC_IP`: Optional. Static public IP returned by `/proxy/ip` and substituted for `{mediaflow_ip}` in forwarded requests. Auto-detected from `api.ipify.org` / `checkip.amazonaws.com` on first use when not set.
33+
- `FORWARD_ALLOWED_HOSTS`: Optional. Comma-separated allowlist of hostnames for `/proxy/forward`. Empty = allow any host (default).
34+
- `FORWARD_DENIED_HOSTS`: Optional. Comma-separated denylist of additional hostnames. Private/loopback IPs are always blocked regardless of this setting.
35+
- `FORWARD_MAX_REQUEST_BODY_BYTES`: Optional. Maximum incoming request body size in bytes. Default: `52428800` (50 MB).
36+
- `FORWARD_MAX_RESPONSE_BODY_BYTES`: Optional. Maximum upstream response body size in bytes. Default: `10485760` (10 MB).
37+

docs/usage/forward.md

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# Generic HTTP Forward Proxy (`/proxy/forward`)
2+
3+
## Overview
4+
5+
`/proxy/forward` is a transparent HTTP relay endpoint. It forwards any HTTP request — any method, any body, any headers — to a destination URL using **MediaFlow's outbound IP**, then returns the upstream response verbatim.
6+
7+
**Primary use cases:**
8+
9+
- **Debrid IP binding** — Route debrid API calls (RealDebrid, AllDebrid, TorBox, etc.) through MediaFlow so the debrid service records MediaFlow's IP as the TCP source, not the addon server's IP. Combine with the `{mediaflow_ip}` placeholder to keep the `ip=` parameter consistent.
10+
- **Extractor POST requests** — Send JSON or form POST to a video host's extraction endpoint from MediaFlow's IP (resolves [issue #271](https://github.com/mhdzumair/mediaflow-proxy/issues/271)).
11+
- **Any IP-bound API call** — Any HTTP API that must appear to come from the same IP that later fetches the stream.
12+
13+
---
14+
15+
## Endpoint
16+
17+
```
18+
ANY /proxy/forward
19+
?d=<destination_url>
20+
&api_password=<password> # required (or encrypted token)
21+
[&h_<header-name>=<value>] # set outbound request header (repeatable)
22+
[&r_<header-name>=<value>] # override response header (repeatable)
23+
```
24+
25+
### Methods
26+
27+
`GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS`
28+
29+
### Parameters
30+
31+
| Parameter | Required | Description |
32+
|---|---|---|
33+
| `d` | Yes | Destination URL. Must be `http://` or `https://`. |
34+
| `api_password` | Yes* | API password (*if `API_PASSWORD` is configured). |
35+
| `h_<name>` | No | Set a request header sent to the destination. E.g. `h_Authorization=Bearer tok`. |
36+
| `r_<name>` | No | Override a response header returned to the caller. |
37+
38+
### Request body
39+
40+
The incoming request body is forwarded to the destination as-is. No base64, no envelope.
41+
42+
### Response
43+
44+
The upstream HTTP status, headers (minus hop-by-hop), and body are returned verbatim.
45+
46+
---
47+
48+
## `{mediaflow_ip}` placeholder
49+
50+
Embed the literal string `{mediaflow_ip}` anywhere in the destination URL or request body. MediaFlow substitutes it with its own public IP before forwarding.
51+
52+
This keeps the `ip=` parameter that debrid services use for CDN URL binding consistent with the TCP source of the API call.
53+
54+
**Example — RealDebrid `ip=` binding:**
55+
56+
```
57+
GET /proxy/forward
58+
?d=https://api.real-debrid.com/rest/1.0/torrents/instantAvailability/{hash}%3Fip%3D{mediaflow_ip}
59+
&api_password=secret
60+
&h_Authorization=Bearer <rd_token>
61+
```
62+
63+
MediaFlow replaces `{mediaflow_ip}` with e.g. `1.2.3.4` before calling RealDebrid.
64+
65+
---
66+
67+
## Public IP endpoint
68+
69+
```
70+
GET /proxy/ip?api_password=<password>
71+
```
72+
73+
Returns `{"ip": "<public-ip>"}` — MediaFlow's outbound IP. Useful when you need to retrieve MediaFlow's IP before making a separate request (e.g. appending `ip=` to a CDN URL).
74+
75+
Configure a static value with the `PUBLIC_IP` environment variable to skip the auto-detection request.
76+
77+
---
78+
79+
## Safety controls
80+
81+
| Control | Default | Description |
82+
|---|---|---|
83+
| SSRF guard | Always on | Requests to loopback (`127.x`, `::1`) and RFC-1918 private addresses are blocked with `403`. |
84+
| Allowlist | Empty (allow all) | Set `FORWARD_ALLOWED_HOSTS` to restrict forwarding to specific hostnames. |
85+
| Denylist | Empty | Set `FORWARD_DENIED_HOSTS` to block additional hostnames. |
86+
| Request body limit | 50 MB | Set `FORWARD_MAX_REQUEST_BODY_BYTES`. Blocks at the limit with `413`. |
87+
| Response body limit | 10 MB | Set `FORWARD_MAX_RESPONSE_BODY_BYTES`. Returns `502` if exceeded. |
88+
| Auth required | Always | `api_password` or a valid encrypted token is mandatory. |
89+
90+
### IP-disclosure header stripping
91+
92+
The following headers are stripped from the outbound request before forwarding so the caller's IP is never leaked:
93+
94+
`X-Forwarded-For`, `X-Real-IP`, `X-Client-IP`, `True-Client-IP`, `Forwarded`, `CF-Connecting-IP`, `X-Original-Forwarded-For`, `X-Cluster-Client-IP`
95+
96+
---
97+
98+
## Examples
99+
100+
### GET — API call through MediaFlow's IP
101+
102+
```bash
103+
curl "http://localhost:8888/proxy/forward\
104+
?d=https://api.real-debrid.com/rest/1.0/user\
105+
&api_password=secret\
106+
&h_Authorization=Bearer%20YOUR_RD_TOKEN"
107+
```
108+
109+
### POST JSON — extractor endpoint
110+
111+
```bash
112+
curl -X POST \
113+
"http://localhost:8888/proxy/forward\
114+
?d=https://example.com/api/videos/abc123/playback\
115+
&api_password=secret\
116+
&h_Referer=https%3A%2F%2Fexample.com%2F\
117+
&h_Content-Type=application%2Fjson" \
118+
-d '{"fingerprint":{"userAgent":"Mozilla/5.0"}}'
119+
```
120+
121+
### POST form — debrid API
122+
123+
```bash
124+
curl -X POST \
125+
"http://localhost:8888/proxy/forward\
126+
?d=https://api.real-debrid.com/rest/1.0/torrents/addMagnet\
127+
&api_password=secret\
128+
&h_Authorization=Bearer%20YOUR_RD_TOKEN\
129+
&h_Content-Type=application%2Fx-www-form-urlencoded" \
130+
--data-urlencode "magnet=magnet:?xt=urn:btih:..."
131+
```
132+
133+
### DELETE — remove a torrent
134+
135+
```bash
136+
curl -X DELETE \
137+
"http://localhost:8888/proxy/forward\
138+
?d=https://api.real-debrid.com/rest/1.0/torrents/delete/TORRENT_ID\
139+
&api_password=secret\
140+
&h_Authorization=Bearer%20YOUR_RD_TOKEN"
141+
```
142+
143+
### Get MediaFlow's public IP
144+
145+
```bash
146+
curl "http://localhost:8888/proxy/ip?api_password=secret"
147+
# {"ip":"1.2.3.4"}
148+
```
149+
150+
---
151+
152+
## Configuration
153+
154+
New settings added to support `/proxy/forward`. All are optional.
155+
156+
| Environment variable | Default | Description |
157+
|---|---|---|
158+
| `PUBLIC_IP` | *(auto-detected)* | Static public IP returned by `/proxy/ip` and substituted for `{mediaflow_ip}`. Skip auto-detection on startup. |
159+
| `FORWARD_ALLOWED_HOSTS` | `[]` | Comma-separated allowlist of hostnames. Empty = allow any host. |
160+
| `FORWARD_DENIED_HOSTS` | `[]` | Comma-separated denylist of hostnames (in addition to the built-in private-IP guard). |
161+
| `FORWARD_MAX_REQUEST_BODY_BYTES` | `52428800` (50 MB) | Maximum incoming request body size. Allows NZB/torrent file uploads. |
162+
| `FORWARD_MAX_RESPONSE_BODY_BYTES` | `10485760` (10 MB) | Maximum upstream response body size. |
163+
164+
---
165+
166+
## Comparison with `/proxy/stream`
167+
168+
| Feature | `/proxy/stream` | `/proxy/forward` |
169+
|---|---|---|
170+
| Methods | `GET`, `HEAD` | Any (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`, …) |
171+
| Request body | No | Yes (forwarded verbatim) |
172+
| Range / partial content | Yes | No |
173+
| HLS / DASH rewriting | Yes | No |
174+
| IP binding (`{mediaflow_ip}`) | No | Yes |
175+
| Response size limit | No | Yes (10 MB default) |
176+
| Intended use | Video streaming | API calls, extractor POSTs |

docs/usage/overview.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@
99
4. `/proxy/mpd/playlist.m3u8`: Generate HLS playlists from MPD
1010
5. `/proxy/mpd/segment.mp4`: Process and decrypt media segments
1111
6. `/proxy/ip`: Get the public IP address of the MediaFlow Proxy server
12-
7. `/extractor/video`: Extract direct video stream URLs from supported hosts (see [Video extractor](extractor.md))
13-
8. `/playlist/builder`: Build and customize playlists from multiple sources
14-
9. `/proxy/epg`: Proxy and cache XMLTV/EPG data (see [EPG Proxy](#epg-proxy) below)
15-
10. `/proxy/transcode/playlist.m3u8`: Generate HLS VOD playlist for generic stream transcode
16-
11. `/proxy/transcode/init.mp4`: fMP4 init segment for generic transcode playlist
17-
12. `/proxy/transcode/segment.m4s`: fMP4 media segment for generic transcode playlist
18-
13. `/proxy/telegram/transcode/playlist.m3u8`: Generate HLS VOD playlist for Telegram transcode
19-
14. `/proxy/telegram/transcode/init.mp4`: fMP4 init segment for Telegram transcode playlist
20-
15. `/proxy/telegram/transcode/segment.m4s`: fMP4 media segment for Telegram transcode playlist
12+
7. `/proxy/forward`: Generic transparent HTTP relay — any method, any body, any headers; route API calls through MediaFlow's IP (see [Forward proxy](forward.md))
13+
8. `/extractor/video`: Extract direct video stream URLs from supported hosts (see [Video extractor](extractor.md))
14+
9. `/playlist/builder`: Build and customize playlists from multiple sources
15+
10. `/proxy/epg`: Proxy and cache XMLTV/EPG data (see [EPG Proxy](#epg-proxy) below)
16+
11. `/proxy/transcode/playlist.m3u8`: Generate HLS VOD playlist for generic stream transcode
17+
12. `/proxy/transcode/init.mp4`: fMP4 init segment for generic transcode playlist
18+
13. `/proxy/transcode/segment.m4s`: fMP4 media segment for generic transcode playlist
19+
14. `/proxy/telegram/transcode/playlist.m3u8`: Generate HLS VOD playlist for Telegram transcode
20+
15. `/proxy/telegram/transcode/init.mp4`: fMP4 init segment for Telegram transcode playlist
21+
16. `/proxy/telegram/transcode/segment.m4s`: fMP4 media segment for Telegram transcode playlist
2122

2223
## EPG Proxy
2324

mediaflow_proxy/routes/proxy.py

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -491,36 +491,42 @@ async def _resolve_public_ip() -> str | None:
491491
return None
492492

493493

494-
_IP_DISCLOSURE_HEADERS = frozenset({
495-
"x-forwarded-for",
496-
"x-real-ip",
497-
"x-client-ip",
498-
"true-client-ip",
499-
"forwarded",
500-
"cf-connecting-ip",
501-
"x-original-forwarded-for",
502-
"x-cluster-client-ip",
503-
})
504-
505-
_HOP_BY_HOP_HEADERS = frozenset({
506-
"connection",
507-
"keep-alive",
508-
"proxy-authenticate",
509-
"proxy-authorization",
510-
"te",
511-
"trailers",
512-
"transfer-encoding",
513-
"upgrade",
514-
})
494+
_IP_DISCLOSURE_HEADERS = frozenset(
495+
{
496+
"x-forwarded-for",
497+
"x-real-ip",
498+
"x-client-ip",
499+
"true-client-ip",
500+
"forwarded",
501+
"cf-connecting-ip",
502+
"x-original-forwarded-for",
503+
"x-cluster-client-ip",
504+
}
505+
)
506+
507+
_HOP_BY_HOP_HEADERS = frozenset(
508+
{
509+
"connection",
510+
"keep-alive",
511+
"proxy-authenticate",
512+
"proxy-authorization",
513+
"te",
514+
"trailers",
515+
"transfer-encoding",
516+
"upgrade",
517+
}
518+
)
515519

516520
# Headers that callers must not inject via h_* params — they enable host-header
517521
# injection, HTTP request smuggling, or break the session's own framing logic.
518-
_BLOCKED_REQUEST_HEADERS = frozenset({
519-
"host",
520-
"content-length",
521-
"transfer-encoding",
522-
"content-encoding",
523-
})
522+
_BLOCKED_REQUEST_HEADERS = frozenset(
523+
{
524+
"host",
525+
"content-length",
526+
"transfer-encoding",
527+
"content-encoding",
528+
}
529+
)
524530

525531

526532
def _check_forward_destination(destination: str) -> None:
@@ -623,11 +629,7 @@ async def proxy_forward_endpoint(
623629
if len(resp_body) > max_response_bytes:
624630
raise HTTPException(status_code=502, detail="Upstream response too large")
625631

626-
resp_headers = {
627-
k: v
628-
for k, v in upstream_resp.headers.items()
629-
if k.lower() not in _HOP_BY_HOP_HEADERS
630-
}
632+
resp_headers = {k: v for k, v in upstream_resp.headers.items() if k.lower() not in _HOP_BY_HOP_HEADERS}
631633
resp_headers.update(proxy_headers.response)
632634

633635
return Response(

0 commit comments

Comments
 (0)