sxyprn fixes-ish
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
334
src/util/dean_edwards.rs
Normal 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: 2–62)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 2–62.
|
||||
/// 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");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user