ltfc.net auth_key problem
Previously(https://github.com/lovasoa/dezoomify-rs/issues/128), this website used URL signing, but now they have increased the difficulty by using an auth_key like .../18/22_2.jpg?auth_key=1741087702-0-0-b68ca3011ed9325b69f12e24ecc9ccaf. How can I download the painting under this situation? Thank you.
Sample: https://ltfc.net/view/SUHA/674887cb078ca419ca0a62e4
I have reversed their signature scheme, in case anyone is interested to implement it in dezoomify
def sign_aliyun_url(url):
m = re.match(r'^(https*:\/\/[\w\-\.]*)(\/.*\.(jpeg|jpg|png))\?*(.*)$', url)
domain, path, _, query = m.groups()
query_str = f"{query}&" if query else ''
timestamp = int(time.time())
signature = hashlib.md5(f"{path}-{timestamp}-0-0-ltfcdotnet".encode()).hexdigest()
return f"{domain}{path}?{query_str}auth_key={timestamp}-0-0-{signature}"
small test script
"""
URL Authentication Module
This module provides functionality to sign URLs for secure access to resources.
It implements two different signing methods:
1. Aliyun OSS authentication - Uses a timestamp and custom signature
2. CAG authentication - Uses a different signature format
The module detects which authentication method to use based on the URL pattern
and the specified source type.
"""
import re
import time
import hashlib
from urllib.parse import quote
def sign_url(url, source_type):
"""
Main function to sign a URL based on the source type
Args:
url (str): The URL to be signed
source_type (str): The source type, e.g., "TILES_SOURCE_ALIYUN"
Returns:
str: The signed URL
"""
if source_type == "TILES_SOURCE_ALIYUN":
return sign_aliyun_url(url)
else:
return sign_cag_url(url)
def sign_aliyun_url(url):
"""
Signs a URL using Aliyun OSS authentication method
Args:
url (str): The URL to sign
Returns:
str: The signed URL
"""
if not url:
return url
aliyun_url_pattern = r'^(https*:\/\/[\w\-\.]*)(\/.*\.(jpeg|jpg|png))\?*(.*)$'
matches = re.match(aliyun_url_pattern, url)
if not matches:
return url
domain = matches.group(1)
path = matches.group(2)
query_params = matches.group(4) or ''
query_string = f"{query_params}&" if query_params else ''
timestamp = int(time.time())
nonce1 = 0
nonce2 = 0
auth_key = generate_auth_key(path, timestamp, nonce1, nonce2)
signature = hashlib.md5(auth_key.encode()).hexdigest()
return f"{domain}{path}?{query_string}auth_key={timestamp}-{nonce1}-{nonce2}-{signature}"
def generate_auth_key(path, timestamp, nonce1, nonce2):
"""
Generates the authentication key for Aliyun URL signing
Args:
path (str): The URL path
timestamp (int): Current timestamp
nonce1 (int): First nonce value
nonce2 (int): Second nonce value
Returns:
str: The authentication key
"""
return f"{path}-{timestamp}-{nonce1}-{nonce2}-ltfcdotnet"
def sign_cag_url(url):
"""
Signs a URL using CAG authentication method
Args:
url (str): The URL to sign
Returns:
str: The signed URL
"""
cag_url_pattern = r'^(http.*\/\/[^\/]*)(\/.*\.(jpg|jpeg))\?*(.*)$'
matches = re.match(cag_url_pattern, url)
if not matches:
return url
domain = matches.group(1)
path = matches.group(2)
query_params = matches.group(4) or ''
secret_key = "b49b4d8a45b8f098ba881d98abbb5c892f8b5c98"
timestamp = format(int(time.time() // 31536000 * 31536000), 'x')
sign_string = secret_key + quote(path) + timestamp
signature = hashlib.md5(sign_string.encode()).hexdigest()
return f"{domain}{path}?{query_params}&sign={signature}&t={timestamp}"
def main():
"""
Main function to handle command line arguments and sign URLs
"""
import argparse
parser = argparse.ArgumentParser(description='Sign URLs for secure access to resources')
parser.add_argument('url', help='The URL to sign')
args = parser.parse_args()
# Auto-detect source type if requested
source_type = "TILES_SOURCE_CAG"
aliyun_keywords = ['aliyun', 'oss', 'cagstore']
lower_url = args.url.lower()
if any(keyword in lower_url for keyword in aliyun_keywords):
source_type = "TILES_SOURCE_ALIYUN"
# Sign the URL
signed_url = sign_url(args.url, source_type)
# Print the result
print(signed_url)
if __name__ == "__main__":
main()
thanks @lovasoa . I tried using Windsurf to fix it, and surprisingly it worked.
url_signer.rs
use std::time::{SystemTime, UNIX_EPOCH};
use md5::{Context, Digest};
pub fn sign_ltfc_url(url: &str) -> String {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
// Extract the path from the full URL
let path = if let Some(idx) = url.find("://") {
if let Some(domain_end) = url[idx + 3..].find('/') {
&url[idx + 3 + domain_end..]
} else {
"/" // fallback to root path if no path found
}
} else {
url // if no scheme found, assume it's already a path
};
let signature_input = format!("{}-{}-0-0-ltfcdotnet", path, timestamp);
let mut context = Context::new();
context.consume(signature_input.as_bytes());
let signature = format!("{:x}", context.compute());
format!("{}?auth_key={}-0-0-{}", url, timestamp, signature)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sign_ltfc_url() {
let url = "https://cag-ac.ltfc.net/cagstore/123/18/1_2.jpg";
let signed_url = sign_ltfc_url(url);
// Basic URL structure tests
assert!(signed_url.starts_with(url));
assert!(signed_url.contains("?auth_key="));
// Extract timestamp and signature from the signed URL
let auth_part = signed_url.split("auth_key=").nth(1).unwrap();
let parts: Vec<&str> = auth_part.split('-').collect();
assert_eq!(parts.len(), 4, "auth_key should have 4 parts separated by '-'");
let timestamp = parts[0].parse::<u64>().unwrap();
let zero1 = parts[1];
let zero2 = parts[2];
let signature = parts[3];
// Verify the fixed parts
assert_eq!(zero1, "0");
assert_eq!(zero2, "0");
// Verify signature format (should be 32 characters, hex)
assert_eq!(signature.len(), 32, "MD5 signature should be 32 characters");
assert!(signature.chars().all(|c| c.is_ascii_hexdigit()),
"Signature should only contain hex characters");
// Verify timestamp is recent
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
assert!(timestamp <= now && timestamp >= now - 10,
"Timestamp should be within 10 seconds of current time");
}
}
Why closing this ? The support is still not implemented in dezoomify or dezoomify-rs
And what do you mean by "fix it" ? Did you have troubles running the above python script?
I successfully ran it locally and can output the correct complete image. It might involve modifications and additions to multiple files, but I am not a programmer and not familiar enough with git/PR operations. On the other hand, I am also concerned that the AI-generated code might not be “qualified” enough. Please tell me how I should contribute (maybe I can try to submit a PR).