header bugfix
This commit is contained in:
@@ -33,10 +33,19 @@ def get_impersonate_session():
|
|||||||
return sess
|
return sess
|
||||||
|
|
||||||
# Stream params that have dedicated meaning and must never be treated as headers.
|
# Stream params that have dedicated meaning and must never be treated as headers.
|
||||||
STREAM_RESERVED_PARAMS = {'url'}
|
# `referer` is mapped to a real Referer header by collect_passthrough_headers, but
|
||||||
|
# `live` is purely a playback hint and must not leak upstream as a header.
|
||||||
|
STREAM_RESERVED_PARAMS = {'url', 'live'}
|
||||||
# Headers that affect the transport layer rather than the resource itself; allowing
|
# Headers that affect the transport layer rather than the resource itself; allowing
|
||||||
# these to be forwarded could enable request smuggling or vhost-routing abuse.
|
# these to be forwarded could enable request smuggling or vhost-routing abuse.
|
||||||
STREAM_DISALLOWED_HEADER_NAMES = {'host', 'content-length', 'transfer-encoding', 'connection', 'expect'}
|
STREAM_DISALLOWED_HEADER_NAMES = {'host', 'content-length', 'transfer-encoding', 'connection', 'expect'}
|
||||||
|
# Headers curl_cffi sets coherently for the impersonated browser. Forwarding the
|
||||||
|
# client's (or extractor's) own values for these would contradict the spoofed TLS
|
||||||
|
# fingerprint and defeat impersonation, so they are never relayed upstream.
|
||||||
|
STREAM_IMPERSONATION_MANAGED_HEADERS = {
|
||||||
|
'user-agent', 'accept', 'accept-encoding', 'accept-language',
|
||||||
|
'sec-ch-ua', 'sec-ch-ua-mobile', 'sec-ch-ua-platform',
|
||||||
|
}
|
||||||
# RFC 7230 token charset for header field-names.
|
# RFC 7230 token charset for header field-names.
|
||||||
HEADER_NAME_RE = re.compile(r"^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$")
|
HEADER_NAME_RE = re.compile(r"^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$")
|
||||||
# Reject control characters (CR/LF/NUL etc.) that could be used for header injection.
|
# Reject control characters (CR/LF/NUL etc.) that could be used for header injection.
|
||||||
@@ -430,6 +439,17 @@ def stream_video():
|
|||||||
headers['Cookie'] = request.headers['Cookie']
|
headers['Cookie'] = request.headers['Cookie']
|
||||||
dbg("forwarding cookies")
|
dbg("forwarding cookies")
|
||||||
|
|
||||||
|
# Relay the per-format headers (e.g. Cookie) the frontend forwarded as
|
||||||
|
# query params so cookie/token-authorized origins serve the media.
|
||||||
|
# Referer is already set above, and impersonation-managed headers are left
|
||||||
|
# to curl_cffi to keep the request coherent with the spoofed fingerprint.
|
||||||
|
for key, value in collect_passthrough_headers(request.args).items():
|
||||||
|
lower = key.lower()
|
||||||
|
if lower == 'referer' or lower in STREAM_IMPERSONATION_MANAGED_HEADERS:
|
||||||
|
continue
|
||||||
|
if value:
|
||||||
|
headers[key] = value
|
||||||
|
|
||||||
# Remove keys with None values
|
# Remove keys with None values
|
||||||
return {k: v for k, v in headers.items() if v}
|
return {k: v for k, v in headers.items() if v}
|
||||||
|
|
||||||
@@ -556,10 +576,27 @@ def stream_video():
|
|||||||
except LookupError:
|
except LookupError:
|
||||||
return body_bytes.decode("utf-8", errors="replace")
|
return body_bytes.decode("utf-8", errors="replace")
|
||||||
|
|
||||||
|
def passthrough_param_suffix():
|
||||||
|
# The relayed headers (e.g. Cookie) the upstream needs for authorization,
|
||||||
|
# encoded as &Name=value so they ride along on every proxied child URL
|
||||||
|
# (variant playlists, segments). Referer is appended separately by each
|
||||||
|
# rewriter; impersonation-managed headers stay with curl_cffi.
|
||||||
|
parts = []
|
||||||
|
for key, value in collect_passthrough_headers(request.args).items():
|
||||||
|
lower = key.lower()
|
||||||
|
if lower == 'referer' or lower in STREAM_IMPERSONATION_MANAGED_HEADERS:
|
||||||
|
continue
|
||||||
|
if not value:
|
||||||
|
continue
|
||||||
|
parts.append(f"&{urllib.parse.quote(key)}={urllib.parse.quote(str(value))}")
|
||||||
|
return ''.join(parts)
|
||||||
|
|
||||||
def rewrite_hls_playlist(body_text, base_url, referer):
|
def rewrite_hls_playlist(body_text, base_url, referer):
|
||||||
|
extra = passthrough_param_suffix()
|
||||||
|
|
||||||
def proxied_url(target):
|
def proxied_url(target):
|
||||||
absolute = urljoin(base_url, target)
|
absolute = urljoin(base_url, target)
|
||||||
return f"/api/stream?url={urllib.parse.quote(absolute, safe='')}&referer={urllib.parse.quote(referer, safe='')}"
|
return f"/api/stream?url={urllib.parse.quote(absolute, safe='')}&referer={urllib.parse.quote(referer, safe='')}{extra}"
|
||||||
|
|
||||||
lines = body_text.splitlines()
|
lines = body_text.splitlines()
|
||||||
rewritten = []
|
rewritten = []
|
||||||
@@ -720,9 +757,11 @@ def stream_video():
|
|||||||
if not video_fmts:
|
if not video_fmts:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
extra = passthrough_param_suffix()
|
||||||
|
|
||||||
def proxied(url):
|
def proxied(url):
|
||||||
return (f"/api/stream?url={urllib.parse.quote(url, safe='')}"
|
return (f"/api/stream?url={urllib.parse.quote(url, safe='')}"
|
||||||
f"&referer={urllib.parse.quote(referer, safe='')}")
|
f"&referer={urllib.parse.quote(referer, safe='')}{extra}")
|
||||||
|
|
||||||
lines = ['#EXTM3U', '#EXT-X-VERSION:3']
|
lines = ['#EXTM3U', '#EXT-X-VERSION:3']
|
||||||
|
|
||||||
|
|||||||
@@ -229,10 +229,7 @@ App.feed = App.feed || {};
|
|||||||
|
|
||||||
const resolved = App.videos.resolveStreamSource(videoData);
|
const resolved = App.videos.resolveStreamSource(videoData);
|
||||||
if (!resolved.url) return;
|
if (!resolved.url) return;
|
||||||
const refererParam = resolved.referer ? `&referer=${encodeURIComponent(resolved.referer)}` : '';
|
const streamUrl = App.videos.buildStreamUrlFromSource(resolved);
|
||||||
const userAgentParam = resolved.userAgent ? `&User-Agent=${encodeURIComponent(resolved.userAgent)}` : '';
|
|
||||||
const liveParam = resolved.isLive ? '&live=1' : '';
|
|
||||||
const streamUrl = `/api/stream?url=${encodeURIComponent(resolved.url)}${refererParam}${userAgentParam}${liveParam}`;
|
|
||||||
const isHls = resolved.isLive ? true : /\.m3u8($|\?)/i.test(resolved.url);
|
const isHls = resolved.isLive ? true : /\.m3u8($|\?)/i.test(resolved.url);
|
||||||
|
|
||||||
video.muted = state.feedMuted;
|
video.muted = state.feedMuted;
|
||||||
|
|||||||
@@ -157,10 +157,7 @@ App.player = App.player || {};
|
|||||||
if (entry.direct) {
|
if (entry.direct) {
|
||||||
streamUrl = resolved.url;
|
streamUrl = resolved.url;
|
||||||
} else {
|
} else {
|
||||||
const refererParam = resolved.referer ? `&referer=${encodeURIComponent(resolved.referer)}` : '';
|
streamUrl = App.videos.buildStreamUrlFromSource(resolved);
|
||||||
const userAgentParam = resolved.userAgent ? `&User-Agent=${encodeURIComponent(resolved.userAgent)}` : '';
|
|
||||||
const liveParam = resolved.isLive ? '&live=1' : '';
|
|
||||||
streamUrl = `/api/stream?url=${encodeURIComponent(resolved.url)}${refererParam}${userAgentParam}${liveParam}`;
|
|
||||||
}
|
}
|
||||||
let isHls = /\.m3u8($|\?)/i.test(resolved.url);
|
let isHls = /\.m3u8($|\?)/i.test(resolved.url);
|
||||||
let isDirectMedia = /\.(mp4|m4v|m4s|webm|ts|mov)($|\?)/i.test(resolved.url);
|
let isDirectMedia = /\.(mp4|m4v|m4s|webm|ts|mov)($|\?)/i.test(resolved.url);
|
||||||
|
|||||||
@@ -860,6 +860,23 @@ App.videos = App.videos || {};
|
|||||||
return headers[name] || headers[name.toLowerCase()] || '';
|
return headers[name] || headers[name.toLowerCase()] || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Merge the resource-level (meta) and format-level http_headers into a single
|
||||||
|
// map so every upstream header the extractor attached (Referer, User-Agent,
|
||||||
|
// Cookie, etc.) can be relayed to the stream proxy. Format-level headers win
|
||||||
|
// on conflict since they describe the specific media URL.
|
||||||
|
const mergeHeaders = function(metaHeaders, fmtHeaders) {
|
||||||
|
const merged = {};
|
||||||
|
[metaHeaders, fmtHeaders].forEach((headers) => {
|
||||||
|
if (!headers || typeof headers !== 'object') return;
|
||||||
|
Object.keys(headers).forEach((name) => {
|
||||||
|
const value = headers[name];
|
||||||
|
if (value === undefined || value === null || value === '') return;
|
||||||
|
merged[name] = String(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return merged;
|
||||||
|
};
|
||||||
|
|
||||||
const deriveReferer = function(url) {
|
const deriveReferer = function(url) {
|
||||||
if (!url) return '';
|
if (!url) return '';
|
||||||
try {
|
try {
|
||||||
@@ -958,7 +975,8 @@ App.videos = App.videos || {};
|
|||||||
const explicitReferer = headerValue(fmt.http_headers, 'Referer') || metaReferer;
|
const explicitReferer = headerValue(fmt.http_headers, 'Referer') || metaReferer;
|
||||||
const referer = explicitReferer || deriveReferer(fmt.url);
|
const referer = explicitReferer || deriveReferer(fmt.url);
|
||||||
const userAgent = headerValue(fmt.http_headers, 'User-Agent') || metaUserAgent;
|
const userAgent = headerValue(fmt.http_headers, 'User-Agent') || metaUserAgent;
|
||||||
return { url: fmt.url, referer, userAgent, isLive, refererRequired: !!explicitReferer };
|
const headers = mergeHeaders(meta.http_headers, fmt.http_headers);
|
||||||
|
return { url: fmt.url, referer, userAgent, headers, isLive, refererRequired: !!explicitReferer };
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!sources.length) {
|
if (!sources.length) {
|
||||||
@@ -968,6 +986,7 @@ App.videos = App.videos || {};
|
|||||||
url: fallbackUrl,
|
url: fallbackUrl,
|
||||||
referer: metaReferer || deriveReferer(fallbackUrl),
|
referer: metaReferer || deriveReferer(fallbackUrl),
|
||||||
userAgent: metaUserAgent,
|
userAgent: metaUserAgent,
|
||||||
|
headers: mergeHeaders(meta.http_headers, null),
|
||||||
isLive,
|
isLive,
|
||||||
refererRequired: !!metaReferer
|
refererRequired: !!metaReferer
|
||||||
});
|
});
|
||||||
@@ -1131,10 +1150,28 @@ App.videos = App.videos || {};
|
|||||||
// by the backend as request headers, so use real header names here.
|
// by the backend as request headers, so use real header names here.
|
||||||
App.videos.buildStreamUrlFromSource = function(resolved) {
|
App.videos.buildStreamUrlFromSource = function(resolved) {
|
||||||
if (!resolved || !resolved.url) return '';
|
if (!resolved || !resolved.url) return '';
|
||||||
const refererParam = resolved.referer ? `&referer=${encodeURIComponent(resolved.referer)}` : '';
|
const params = [];
|
||||||
const userAgentParam = resolved.userAgent ? `&User-Agent=${encodeURIComponent(resolved.userAgent)}` : '';
|
// Referer keeps its dedicated lowercase param (the backend maps it back to
|
||||||
const liveParam = resolved.isLive ? '&live=1' : '';
|
// `Referer`) so the derived-referer fallback in resolveStreamSources is
|
||||||
return `/api/stream?url=${encodeURIComponent(resolved.url)}${refererParam}${userAgentParam}${liveParam}`;
|
// honoured even when no explicit Referer header was present.
|
||||||
|
if (resolved.referer) params.push(`referer=${encodeURIComponent(resolved.referer)}`);
|
||||||
|
if (resolved.userAgent) params.push(`User-Agent=${encodeURIComponent(resolved.userAgent)}`);
|
||||||
|
// Relay every other upstream header the extractor attached (e.g. Cookie).
|
||||||
|
// Referer/User-Agent are already emitted above, so skip them here to avoid
|
||||||
|
// duplicating the same header under two query keys.
|
||||||
|
const headers = resolved.headers;
|
||||||
|
if (headers && typeof headers === 'object') {
|
||||||
|
Object.keys(headers).forEach((name) => {
|
||||||
|
const lower = name.toLowerCase();
|
||||||
|
if (lower === 'referer' || lower === 'user-agent') return;
|
||||||
|
const value = headers[name];
|
||||||
|
if (value === undefined || value === null || value === '') return;
|
||||||
|
params.push(`${encodeURIComponent(name)}=${encodeURIComponent(value)}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (resolved.isLive) params.push('live=1');
|
||||||
|
const query = params.length ? `&${params.join('&')}` : '';
|
||||||
|
return `/api/stream?url=${encodeURIComponent(resolved.url)}${query}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
App.videos.buildStreamUrl = function(videoOrUrl, options) {
|
App.videos.buildStreamUrl = function(videoOrUrl, options) {
|
||||||
|
|||||||
Reference in New Issue
Block a user