Parse expiry timestamp from spotifycdn.com CDN URLs (Fixes #1182) (#1183)

The CDN URLs list now includes spotifycdn.com which has a different
format. It was being erroneously interpreted using the scdn.co format
and trying to parse non-digit characters as a timestamp.

Also ignore expiry timestamps we can't parse for future new URLs.
This commit is contained in:
Nick Steel 2023-07-04 09:37:22 +01:00 committed by GitHub
parent 4d6de15a97
commit c491f90e09
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -5,6 +5,7 @@ use std::{
use protobuf::Message;
use thiserror::Error;
use time::Duration;
use url::Url;
use super::{date::Date, Error, FileId, Session};
@ -16,6 +17,8 @@ use protocol::storage_resolve::StorageResolveResponse as CdnUrlMessage;
#[derive(Debug, Clone)]
pub struct MaybeExpiringUrl(pub String, pub Option<Date>);
const CDN_URL_EXPIRY_MARGIN: Duration = Duration::seconds(5 * 60);
#[derive(Debug, Clone)]
pub struct MaybeExpiringUrls(pub Vec<MaybeExpiringUrl>);
@ -114,55 +117,96 @@ impl TryFrom<CdnUrlMessage> for MaybeExpiringUrls {
.iter()
.map(|cdn_url| {
let url = Url::parse(cdn_url)?;
let mut expiry: Option<Date> = None;
if is_expiring {
let expiry_str = if let Some(token) = url
let mut expiry_str: Option<String> = None;
if let Some(token) = url
.query_pairs()
.into_iter()
.find(|(key, _value)| key == "__token__")
{
//"https://audio-ak-spotify-com.akamaized.net/audio/4712bc9e47f7feb4ee3450ef2bb545e1d83c3d54?__token__=exp=1688165560~hmac=4e661527574fab5793adb99cf04e1c2ce12294c71fe1d39ffbfabdcfe8ce3b41",
if let Some(mut start) = token.1.find("exp=") {
start += 4;
if token.1.len() >= start {
let slice = &token.1[start..];
if let Some(end) = slice.find('~') {
// this is the only valid invariant for akamaized.net
String::from(&slice[..end])
expiry_str = Some(String::from(&slice[..end]));
} else {
String::from(slice)
expiry_str = Some(String::from(slice));
}
} else {
String::new()
}
} else {
String::new()
}
} else if let Some(token) = url
.query_pairs()
.into_iter()
.find(|(key, _value)| key == "Expires")
{
//"https://audio-gm-off.spotifycdn.com/audio/4712bc9e47f7feb4ee3450ef2bb545e1d83c3d54?Expires=1688165560~FullPath~hmac=IIZA28qptl8cuGLq15-SjHKHtLoxzpy_6r_JpAU4MfM=",
if let Some(end) = token.1.find('~') {
// this is the only valid invariant for spotifycdn.com
let slice = &token.1[..end];
expiry_str = Some(String::from(&slice[..end]));
}
} else if let Some(query) = url.query() {
//"https://audio4-fa.scdn.co/audio/4712bc9e47f7feb4ee3450ef2bb545e1d83c3d54?1688165560_0GKSyXjLaTW1BksFOyI4J7Tf9tZDbBUNNPu9Mt4mhH4=",
let mut items = query.split('_');
if let Some(first) = items.next() {
// this is the only valid invariant for scdn.co
String::from(first)
expiry_str = Some(String::from(first));
}
}
if let Some(exp_str) = expiry_str {
if let Ok(expiry_parsed) = exp_str.parse::<i64>() {
if let Ok(expiry_at) = Date::from_timestamp_ms(expiry_parsed * 1_000) {
let with_margin = expiry_at.saturating_sub(CDN_URL_EXPIRY_MARGIN);
expiry = Some(Date::from(with_margin));
}
} else {
String::new()
warn!("Cannot parse CDN URL expiry timestamp '{exp_str}' from '{cdn_url}'");
}
} else {
String::new()
};
let mut expiry: i64 = expiry_str.parse()?;
expiry -= 5 * 60; // seconds
Ok(MaybeExpiringUrl(
cdn_url.to_owned(),
Some(Date::from_timestamp_ms(expiry * 1000)?),
))
} else {
Ok(MaybeExpiringUrl(cdn_url.to_owned(), None))
warn!("Unknown CDN URL format: {cdn_url}");
}
}
Ok(MaybeExpiringUrl(cdn_url.to_owned(), expiry))
})
.collect::<Result<Vec<MaybeExpiringUrl>, Error>>()?;
Ok(Self(result))
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_maybe_expiring_urls() {
let timestamp = 1688165560;
let mut msg = CdnUrlMessage::new();
msg.result = StorageResolveResponse_Result::CDN.into();
msg.cdnurl = vec![
format!("https://audio-ak-spotify-com.akamaized.net/audio/foo?__token__=exp={timestamp}~hmac=4e661527574fab5793adb99cf04e1c2ce12294c71fe1d39ffbfabdcfe8ce3b41"),
format!("https://audio-gm-off.spotifycdn.com/audio/foo?Expires={timestamp}~FullPath~hmac=IIZA28qptl8cuGLq15-SjHKHtLoxzpy_6r_JpAU4MfM="),
format!("https://audio4-fa.scdn.co/audio/foo?{timestamp}_0GKSyXjLaTW1BksFOyI4J7Tf9tZDbBUNNPu9Mt4mhH4="),
"https://audio4-fa.scdn.co/foo?baz".to_string(),
];
msg.fileid = vec![0];
let urls = MaybeExpiringUrls::try_from(msg).expect("valid urls");
assert_eq!(urls.len(), 4);
assert!(urls[0].1.is_some());
assert!(urls[1].1.is_some());
assert!(urls[2].1.is_some());
assert!(urls[3].1.is_none());
let timestamp_margin = Duration::seconds(timestamp) - CDN_URL_EXPIRY_MARGIN;
assert_eq!(
urls[0].1.unwrap().as_timestamp_ms() as i128,
timestamp_margin.whole_milliseconds()
);
}
}