sxyprn fixes-ish

This commit is contained in:
Simon
2026-05-11 13:32:08 +00:00
committed by ForgeCode
parent b4774a0c0f
commit 5ba16ab338
6 changed files with 380 additions and 34 deletions

View File

@@ -476,6 +476,15 @@ impl SxyprnProvider {
let mut formats = vec![];
// Add sxyprn format
let sxyprn_url = format!(
"{}/proxy/sxyprn/post/{}",
options.public_url_base.as_deref().unwrap_or(""),
id
);
formats.push(
VideoFormat::new(sxyprn_url.clone(), "auto".to_string(), "mp4".to_string())
.format_note(sxyprn_url.split("/").nth(4).unwrap_or("sxyprn").to_string()),
);
let doodstream_urls: Vec<String> = title_links
.iter()
@@ -491,32 +500,24 @@ impl SxyprnProvider {
);
}
let lulustream_urls: Vec<String> = title_links
.iter()
.filter(|url| proxy_name_for_url(url).as_deref() == Some("lulustream"))
.map(|url| rewrite_hoster_url(options, url))
.collect();
// let lulustream_urls: Vec<String> = title_links
// .iter()
// .filter(|url| proxy_name_for_url(url).as_deref() == Some("lulustream"))
// .map(|url| rewrite_hoster_url(options, url))
// .collect();
for lulustream_url in lulustream_urls {
formats.push(
VideoFormat::m3u8(
lulustream_url.clone(),
"auto".to_string(),
"m3u8".to_string(),
)
.format_note("lulustream".to_string())
.format_id("lulustream".to_string()),
);
}
let sxyprn_url = format!(
"{}/proxy/sxyprn/post/{}",
options.public_url_base.as_deref().unwrap_or(""),
id
);
formats.push(
VideoFormat::new(sxyprn_url.clone(), "auto".to_string(), "mp4".to_string())
.format_note(sxyprn_url.split("/").nth(4).unwrap_or("sxyprn").to_string()),
);
// for lulustream_url in lulustream_urls {
// formats.push(
// VideoFormat::m3u8(
// lulustream_url.clone(),
// "auto".to_string(),
// "m3u8".to_string(),
// )
// .format_note("lulustream".to_string())
// .format_id("lulustream".to_string()),
// );
// }
// Also collect and transform vidara.so URLs to proxy format and add as formats
let vidara_urls: Vec<String> = title_links
.iter()
@@ -534,7 +535,7 @@ impl SxyprnProvider {
let mut video_item = VideoItem::new(
id.clone(),
title,
format!("{}/post/{}", self.url, id.clone()),
url.clone(),
"sxyprn".to_string(),
thumb,
duration,

View File

@@ -2,7 +2,7 @@ use ntex::web;
use url::Url;
use serde_json::json;
use crate::util::requester::Requester;
use crate::util::{dean_edwards, requester::Requester};
#[derive(Debug, Clone)]
pub struct LulustreamProxy {}
@@ -51,7 +51,7 @@ impl LulustreamProxy {
return false;
};
(host == "lulustream.com" || host == "www.lulustream.com" || host == "luluvdo.com")
&& (parsed.path().starts_with("/v/")||parsed.path().starts_with("/e/"))
&& !parsed.path().is_empty() && parsed.path() != "/"
}
pub async fn get_video_url(
@@ -64,15 +64,19 @@ impl LulustreamProxy {
println!("LulustreamProxy: Invalid detail URL: {url}");
return String::new();
};
let text = requester.get(&detail_url, None).await.unwrap_or_default();
let video_url = text.split("sources: [{file:\"")
let mut text = requester.get(&detail_url, None).await.unwrap_or_default();
if !text.contains("[{file:\"") {
let packedtext = text.split("<script type='text/javascript'>").nth(1).and_then(|t| t.split("</script>").next()).unwrap_or_default();
println!("LulustreamProxy: Found packed text: {packedtext}");
text = dean_edwards::unpack(&packedtext).unwrap_or_default();
println!("LulustreamProxy: Unpacked text: {text}");
}
let video_url = text.split("[{file:\"")
.nth(1)
.and_then(|s| s.split('"').next())
.unwrap_or_default()
.to_string();
if video_url.is_empty() {
println!("LulustreamProxy: Failed to extract video URL for video ID: {video_id}");
}
println!("LulustreamProxy: Extracted video URL: {video_url}");
video_url
}
}

View File

@@ -75,7 +75,6 @@ impl SxyprnProxy {
Ok(None) => println!("No redirect found for {}", sxyprn_video_url),
Err(e) => eprintln!("Request failed: {}", e),
}
return "".to_string();
}
}

View File

@@ -13,6 +13,7 @@ use crate::proxies::spankbang::SpankbangProxy;
use crate::proxies::sxyprn::SxyprnProxy;
use crate::proxies::vjav::VjavProxy;
use crate::proxies::vidara::VidaraProxy;
use crate::proxies::lulustream::LulustreamProxy;
use crate::proxies::*;
use crate::util::requester::Requester;
@@ -27,6 +28,11 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.route(web::post().to(proxy2redirect))
.route(web::get().to(proxy2redirect)),
)
.service(
web::resource("/lulustream/{endpoint}*")
.route(web::post().to(proxy2redirect))
.route(web::get().to(proxy2redirect)),
)
.service(
web::resource("/sxyprn/{endpoint}*")
.route(web::post().to(proxy2redirect))
@@ -149,6 +155,7 @@ fn get_proxy(proxy: &str) -> Option<AnyProxy> {
"pimpbunny" => Some(AnyProxy::Pimpbunny(PimpbunnyProxy::new())),
"porndish" => Some(AnyProxy::Porndish(PorndishProxy::new())),
"spankbang" => Some(AnyProxy::Spankbang(SpankbangProxy::new())),
"lulustream" => Some(AnyProxy::Lulustream(LulustreamProxy::new())),
_ => None,
}
}

334
src/util/dean_edwards.rs Normal file
View File

@@ -0,0 +1,334 @@
/// Dean Edwards p,a,c,k,e,d unpacker.
///
/// Mirrors the original JS decoder:
/// while(c--) if(k[c]) p = p.replace(/\b{c.toString(a)}\b/g, k[c]);
///
/// Usage:
/// let source = r#"eval(function(p,a,c,k,e,d){...}('...',36,N,'w0|w1|...'.split('|'),0,{}))"#;
/// let plain = unpack(source)?;
use std::fmt;
// ── Error type ────────────────────────────────────────────────────────────────
#[derive(Debug)]
pub enum UnpackError {
NotPacked,
MalformedArgs(String),
BadBase(u32),
}
impl fmt::Display for UnpackError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotPacked => write!(f, "input does not look like a packed script"),
Self::MalformedArgs(s) => write!(f, "could not parse packed arguments: {s}"),
Self::BadBase(b) => write!(f, "unsupported base {b} (supported: 262)"),
}
}
}
impl std::error::Error for UnpackError {}
// ── Public entry point ────────────────────────────────────────────────────────
/// Detect, parse, and unpack a Dean Edwards packed script.
/// Returns the deobfuscated source on success.
pub fn unpack(input: &str) -> Result<String, UnpackError> {
let args = extract_args(input.trim())?;
decode(args)
}
// ── Argument extraction ───────────────────────────────────────────────────────
struct PackedArgs {
payload: String, // p
base: u32, // a
words: Vec<String>, // k (already split)
}
/// Find the argument list of the outer `function(p,a,c,k,e,d){...}(...)` call
/// and parse p, a, c, k from it.
fn extract_args(input: &str) -> Result<PackedArgs, UnpackError> {
// Locate the opening of the argument tuple: the '(' that directly follows
// the closing '}' of the function body.
let body_end = input
.find("}('") // most common: }('payload',…
.or_else(|| input.find("}(\""))
.ok_or(UnpackError::NotPacked)?;
// args_start points at '(' skip it to reach the opening quote of the payload.
let args_start = body_end + 1;
let args_str = input[args_start..].trim_start_matches('(');
// Pull the payload string (first argument).
let (payload, after_payload) = parse_js_string(args_str)
.ok_or_else(|| UnpackError::MalformedArgs("could not read payload string".into()))?;
// Expect ',base,count,'
let rest = after_payload
.trim_start_matches(',');
let (base_str, rest) = rest
.split_once(',')
.ok_or_else(|| UnpackError::MalformedArgs("missing base".into()))?;
let base: u32 = base_str
.trim()
.parse()
.map_err(|_| UnpackError::MalformedArgs(format!("bad base: {base_str}")))?;
let (count_str, rest) = rest
.split_once(',')
.ok_or_else(|| UnpackError::MalformedArgs("missing count".into()))?;
let count: usize = count_str
.trim()
.parse()
.map_err(|_| UnpackError::MalformedArgs(format!("bad count: {count_str}")))?;
// Parse the dictionary k. Two common forms:
// 'w0|w1|w2'.split('|')
// ["w0","w1","w2"]
let words = parse_dictionary(rest.trim(), count)
.ok_or_else(|| UnpackError::MalformedArgs("could not parse dictionary".into()))?;
Ok(PackedArgs { payload, base, words })
}
// ── Dictionary parser ─────────────────────────────────────────────────────────
fn parse_dictionary(s: &str, count: usize) -> Option<Vec<String>> {
if s.starts_with('\'') || s.starts_with('"') {
// 'w0|w1|…'.split('|') separator can be any single char
let (joined, rest) = parse_js_string(s)?;
// find .split('<sep>')
let sep_start = rest.find(".split(")?;
let after_split = &rest[sep_start + 7..]; // skip `.split(`
let (sep, _) = parse_js_string(after_split.trim())?;
let sep_char = if sep.is_empty() { '|' } else { sep.chars().next().unwrap() };
let words: Vec<String> = joined.split(sep_char).map(str::to_owned).collect();
Some(pad_to(words, count))
} else if s.starts_with('[') {
// ["w0","w1",…]
let end = s.find(']')?;
let inner = &s[1..end];
let words = parse_array_literal(inner);
Some(pad_to(words, count))
} else {
None
}
}
/// Parse a JS array literal (no nesting needed for the k array).
fn parse_array_literal(s: &str) -> Vec<String> {
let mut words = Vec::new();
let mut rest = s.trim();
loop {
rest = rest.trim_start_matches(',').trim();
if rest.is_empty() { break; }
if rest.starts_with('\'') || rest.starts_with('"') {
if let Some((w, after)) = parse_js_string(rest) {
words.push(w);
rest = after.trim();
} else {
break;
}
} else {
// empty slot → push empty string
if let Some(pos) = rest.find(',') {
words.push(String::new());
rest = &rest[pos..];
} else {
break;
}
}
}
words
}
fn pad_to(mut v: Vec<String>, n: usize) -> Vec<String> {
v.resize(n, String::new());
v
}
// ── JS string parser ──────────────────────────────────────────────────────────
/// Parse a single-quoted or double-quoted JS string literal at the start of `s`.
/// Returns (unescaped content, remainder after closing quote).
fn parse_js_string(s: &str) -> Option<(String, &str)> {
let mut chars = s.char_indices();
let (_, quote) = chars.next()?;
if quote != '\'' && quote != '"' { return None; }
let mut result = String::new();
let mut escaped = false;
for (i, ch) in chars {
if escaped {
match ch {
'n' => result.push('\n'),
'r' => result.push('\r'),
't' => result.push('\t'),
'\\' => result.push('\\'),
'\'' => result.push('\''),
'"' => result.push('"'),
_ => { result.push('\\'); result.push(ch); }
}
escaped = false;
} else if ch == '\\' {
escaped = true;
} else if ch == quote {
return Some((result, &s[i + ch.len_utf8()..]));
} else {
result.push(ch);
}
}
None // unclosed string
}
// ── Number → string for arbitrary base ───────────────────────────────────────
/// JavaScript's `Number.prototype.toString(radix)` for bases 262.
/// Digits: 0-9, then a-z (10-35), then A-Z (36-61).
fn num_to_base_str(mut n: usize, base: u32) -> Result<String, UnpackError> {
if base < 2 || base > 62 {
return Err(UnpackError::BadBase(base));
}
if n == 0 { return Ok("0".to_owned()); }
const DIGITS: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
let base = base as usize;
let mut buf = Vec::new();
while n > 0 {
buf.push(DIGITS[n % base] as char);
n /= base;
}
buf.reverse();
Ok(buf.into_iter().collect())
}
// ── Core decode ───────────────────────────────────────────────────────────────
/// JS regex `\b` word-boundary replacement.
/// We use a hand-rolled matcher so we don't need an external crate.
fn replace_word_boundary(haystack: &str, needle: &str, replacement: &str) -> String {
if needle.is_empty() { return haystack.to_owned(); }
let h: Vec<char> = haystack.chars().collect();
let n: Vec<char> = needle.chars().collect();
let nlen = n.len();
let hlen = h.len();
let is_word = |c: char| c.is_ascii_alphanumeric() || c == '_';
let mut out = String::with_capacity(haystack.len());
let mut i = 0;
while i <= hlen.saturating_sub(nlen) {
// Check if h[i..i+nlen] == needle
if h[i..i + nlen] == n[..] {
// Word-boundary checks
let left_ok = i == 0 || !is_word(h[i - 1]);
let right_ok = i + nlen == hlen || !is_word(h[i + nlen]);
if left_ok && right_ok {
out.push_str(replacement);
i += nlen;
continue;
}
}
out.push(h[i]);
i += 1;
}
// Append whatever is left
for ch in &h[i..] { out.push(*ch); }
out
}
fn decode(args: PackedArgs) -> Result<String, UnpackError> {
let PackedArgs { mut payload, base, words } = args;
// Validate base once up front.
if base < 2 || base > 62 {
return Err(UnpackError::BadBase(base));
}
// Mirror the JS: for c from (words.len()-1) down to 0
let count = words.len();
for c in (0..count).rev() {
if words[c].is_empty() { continue; }
let key = num_to_base_str(c, base)?;
payload = replace_word_boundary(&payload, &key, &words[c]);
}
Ok(payload)
}
// ── Tests ─────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
/// Minimal self-contained packed snippet (base 36, 3 words).
/// Original source: `var hello = world + foo;`
/// Packed manually for the test:
/// payload = "var 1 = 2 + 0;"
/// base = 36
/// count = 3
/// words = ["foo", "hello", "world"] (indices 0,1,2)
#[test]
fn test_basic_unpack() {
// Build a fake packed string without the eval wrapper for direct testing.
let args = PackedArgs {
payload: "var 1 = 2 + 0;".to_owned(),
base: 36,
words: vec!["foo".to_owned(), "hello".to_owned(), "world".to_owned()],
};
let result = decode(args).unwrap();
assert_eq!(result, "var hello = world + foo;");
}
#[test]
fn test_word_boundary() {
// "foo10bar" should NOT replace "10", but " 10 " should.
let result = replace_word_boundary("foo10bar baz 10 qux10", "10", "X");
assert_eq!(result, "foo10bar baz X qux10");
}
#[test]
fn test_num_to_base_str() {
assert_eq!(num_to_base_str(0, 36).unwrap(), "0");
assert_eq!(num_to_base_str(10, 36).unwrap(), "a");
assert_eq!(num_to_base_str(35, 36).unwrap(), "z");
assert_eq!(num_to_base_str(36, 36).unwrap(), "10");
assert_eq!(num_to_base_str(255, 16).unwrap(), "ff");
assert_eq!(num_to_base_str(7, 2).unwrap(), "111");
}
#[test]
fn test_parse_split_dictionary() {
let input = r#"'foo|bar|baz'.split('|'),0,{})"#;
let words = parse_dictionary(input, 3).unwrap();
assert_eq!(words, vec!["foo", "bar", "baz"]);
}
#[test]
fn test_parse_array_dictionary() {
let input = r#"["alpha","","gamma"],0,{})"#;
let words = parse_dictionary(input, 3).unwrap();
assert_eq!(words[0], "alpha");
assert_eq!(words[1], "");
assert_eq!(words[2], "gamma");
}
/// Full round-trip with the eval wrapper (base 10, tiny example).
#[test]
fn test_full_eval_wrapper() {
// payload: "0 1 2" words: ["hello","world","rust"] base:10 count:3
let packed = r#"eval(function(p,a,c,k,e,d){while(c--)if(k[c])p=p.replace(new RegExp('\\b'+c.toString(a)+'\\b','g'),k[c]);return p}('0 1 2',10,3,'hello|world|rust'.split('|'),0,{}))"#;
let result = unpack(packed).unwrap();
assert_eq!(result, "hello world rust");
}
}

View File

@@ -9,6 +9,7 @@ pub mod hoster_proxy;
pub mod proxy;
pub mod requester;
pub mod time;
pub mod dean_edwards;
pub fn parse_abbreviated_number(s: &str) -> Option<u32> {
let s = s.trim();