Implement fingerprint-based viewer token generation using IP + user agent hashing (#4)

* Initial plan for issue

* Implement fingerprint-based viewer token generation

Co-authored-by: v0l <1172179+v0l@users.noreply.github.com>

* Fix bech32 encoding and add comprehensive tests

Co-authored-by: v0l <1172179+v0l@users.noreply.github.com>

* Remove bech32 HRP and move dependencies to workspace

Co-authored-by: v0l <1172179+v0l@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: v0l <1172179+v0l@users.noreply.github.com>
This commit is contained in:
Copilot
2025-06-07 16:27:37 +01:00
committed by GitHub
parent 6f276d7e72
commit e097b9caf1
5 changed files with 95 additions and 6 deletions

View File

@ -29,6 +29,8 @@ hex.workspace = true
itertools.workspace = true
futures-util = "0.3.30"
m3u8-rs.workspace = true
sha2.workspace = true
data-encoding.workspace = true
# srt
srt-tokio = { version = "0.4.3", optional = true }

View File

@ -4,6 +4,8 @@ use std::time::{Duration, Instant};
use uuid::Uuid;
use tokio::task;
use log::debug;
use sha2::{Digest, Sha256};
use data_encoding::BASE32_NOPAD;
#[derive(Debug, Clone)]
pub struct ViewerInfo {
@ -35,8 +37,23 @@ impl ViewerTracker {
tracker
}
pub fn generate_viewer_token() -> String {
Uuid::new_v4().to_string()
pub fn generate_viewer_token(ip_address: &str, user_agent: Option<&str>) -> String {
// Create input string by combining IP address and user agent
let input = match user_agent {
Some(ua) => format!("{}{}", ip_address, ua),
None => ip_address.to_string(),
};
// Hash the input using SHA-256
let mut hasher = Sha256::new();
hasher.update(input.as_bytes());
let hash = hasher.finalize();
// Take the first 8 bytes of the hash
let fingerprint = &hash[..8];
// Base32 encode the fingerprint (without padding)
BASE32_NOPAD.encode(fingerprint).to_lowercase()
}
pub fn track_viewer(&self, token: &str, stream_id: &str, ip_address: &str, user_agent: Option<String>) {
@ -111,4 +128,72 @@ impl Default for ViewerTracker {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_viewer_token_consistency() {
// Test that the same IP and user agent always generate the same token
let ip = "192.168.1.1";
let user_agent = Some("Mozilla/5.0 (Test Browser)");
let token1 = ViewerTracker::generate_viewer_token(ip, user_agent);
let token2 = ViewerTracker::generate_viewer_token(ip, user_agent);
assert_eq!(token1, token2, "Same IP and user agent should generate identical tokens");
}
#[test]
fn test_generate_viewer_token_different_inputs() {
// Test that different inputs generate different tokens
let ip1 = "192.168.1.1";
let ip2 = "192.168.1.2";
let user_agent = Some("Mozilla/5.0 (Test Browser)");
let token1 = ViewerTracker::generate_viewer_token(ip1, user_agent);
let token2 = ViewerTracker::generate_viewer_token(ip2, user_agent);
assert_ne!(token1, token2, "Different IPs should generate different tokens");
}
#[test]
fn test_generate_viewer_token_no_user_agent() {
// Test that tokens are generated even without user agent
let ip = "192.168.1.1";
let token1 = ViewerTracker::generate_viewer_token(ip, None);
let token2 = ViewerTracker::generate_viewer_token(ip, None);
assert_eq!(token1, token2, "Same IP without user agent should generate identical tokens");
}
#[test]
fn test_generate_viewer_token_format() {
// Test that generated tokens have the expected base32 format
let ip = "192.168.1.1";
let user_agent = Some("Mozilla/5.0 (Test Browser)");
let token = ViewerTracker::generate_viewer_token(ip, user_agent);
// Should be base32 encoded (lowercase, no padding)
assert!(token.chars().all(|c| "abcdefghijklmnopqrstuvwxyz234567".contains(c)),
"Token should only contain base32 characters");
assert!(token.len() > 10, "Token should be reasonably long");
}
#[test]
fn test_generate_viewer_token_different_user_agents() {
// Test that different user agents generate different tokens
let ip = "192.168.1.1";
let user_agent1 = Some("Mozilla/5.0 (Chrome)");
let user_agent2 = Some("Mozilla/5.0 (Firefox)");
let token1 = ViewerTracker::generate_viewer_token(ip, user_agent1);
let token2 = ViewerTracker::generate_viewer_token(ip, user_agent2);
assert_ne!(token1, token2, "Different user agents should generate different tokens");
}
}

View File

@ -38,7 +38,7 @@ nostr-sdk = { version = "0.38.0" }
fedimint-tonic-lnd = { version = "0.2.0", default-features = false, features = ["invoicesrpc", "versionrpc"] }
reqwest = { version = "0.12.9", features = ["stream", "json"] }
base64 = { version = "0.22.1" }
sha2 = { version = "0.10.8" }
sha2.workspace = true
pretty_env_logger = "0.5.0"
clap = { version = "4.5.16", features = ["derive"] }
futures-util = "0.3.31"

View File

@ -71,8 +71,8 @@ impl HttpServer {
api.track_viewer(token, stream_id, &client_ip, user_agent.clone());
token.clone()
} else {
// Generate new viewer token
let token = ViewerTracker::generate_viewer_token();
// Generate new viewer token based on IP and user agent fingerprint
let token = ViewerTracker::generate_viewer_token(&client_ip, user_agent.as_deref());
api.track_viewer(&token, stream_id, &client_ip, user_agent);
token
};