dezoomify-rs icon indicating copy to clipboard operation
dezoomify-rs copied to clipboard

ltfc.net auth_key problem

Open poorfish opened this issue 9 months ago • 5 comments

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

poorfish avatar Mar 04 '25 11:03 poorfish

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()

lovasoa avatar Mar 04 '25 13:03 lovasoa

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");
    }
}

poorfish avatar Mar 12 '25 09:03 poorfish

Why closing this ? The support is still not implemented in dezoomify or dezoomify-rs

lovasoa avatar Mar 12 '25 10:03 lovasoa

And what do you mean by "fix it" ? Did you have troubles running the above python script?

lovasoa avatar Mar 12 '25 10:03 lovasoa

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).

poorfish avatar Mar 12 '25 10:03 poorfish