├── Cargo.toml ├── LICENSE ├── README.md └── src └── main.rs /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "RustyDLNA" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | 8 | 9 | [profile.release] 10 | opt-level = 3 11 | lto = "fat" 12 | strip = "symbols" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 pj1234678 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RustyDLNA 2 | 3 | RustyDLNA is a dependency-free, purely safe Rust implementation of a DLNA (Digital Living Network Alliance) server, designed for seamless media sharing and streaming across devices. 4 | 5 | ## Features 6 | 7 | - **Zero dependencies**: No external libraries or frameworks required, making it lightweight and easy to integrate. 8 | - **Purely safe**: Written entirely in safe Rust, ensuring memory safety and preventing common errors. 9 | - **DLNA compliance**: Implements core DLNA protocols for smooth media sharing and streaming with compatible devices. 10 | - **Media format support**: Handles a variety of audio and video formats for broad compatibility. 11 | - **Efficient performance**: Optimized for low overhead and high throughput. 12 | - **No conditional statements**: Built using Rust's expressive type system and pattern matching, eliminating the need for `if` statements. 13 | 14 | ## Benefits 15 | 16 | - Lightweight and easy to integrate with existing systems. 17 | - Robust security through Rust’s safety guarantees. 18 | - Compatibility with DLNA-certified devices across platforms. 19 | - Customizable and extensible to suit different use cases. 20 | - Unparalleled code clarity and maintainability. 21 | 22 | ## Design Philosophy 23 | 24 | RustyDLNA is inspired by the principles of wastewater pump PLCs (Programmable Logic Controllers), and follows a design philosophy that prioritizes: 25 | 26 | - **Only using `match` statements** to ensure that all possible cases are explicitly handled, avoiding the ambiguity and risks associated with conditional logic like `if` statements. 27 | 28 | This approach results in: 29 | 30 | - Concise and readable code. 31 | - Improved maintainability, as all potential cases are accounted for. 32 | - Reduced susceptibility to bugs, with clear and exhaustive case handling. 33 | 34 | ## Compatible Players 35 | 36 | RustyDLNA supports a wide range of media players across platforms: 37 | 38 | - **VLC**: All Platforms 39 | - **Kodi**: All platforms 40 | - **SKYBOX**: Meta Quest (VR) 41 | - **Xbox Series Consoles**: Native DLNA support 42 | 43 | ## Platform and Architecture Support 44 | 45 | RustyDLNA compiles and runs on a wide variety of platforms and architectures. All it needs is a network card and a folder to serve media from. Supported platforms and architectures include: 46 | 47 | - **Operating Systems**: 48 | - Linux (all major distros) 49 | - Windows (10, 11) 50 | - macOS (including M1/M2 ARM support) 51 | - Android 52 | - iOS 53 | - Xbox (native DLNA support) 54 | - PlayStation (native DLNA support) 55 | 56 | - **Architectures**: 57 | - x86_64 (64-bit Intel/AMD) 58 | - aarch64 (ARM 64-bit, including Raspberry Pi and ARM-based systems) 59 | - x86 (32-bit Intel/AMD) 60 | - ARM (32-bit, including older Raspberry Pi models) 61 | - MIPS, PowerPC (experimental support) 62 | 63 | Whether you're running RustyDLNA on a powerful desktop, a low-power ARM device, or even a console, it will seamlessly compile and work as long as there is a network card and media directory. 64 | 65 | ## Use Cases 66 | 67 | - Streaming media from Rust applications. 68 | - Creating DLNA-compliant devices or services. 69 | - Integrating DLNA capabilities into existing systems. 70 | 71 | ## Technical Details 72 | 73 | - **Platform support**: Linux, Windows, macOS, Android, iOS, Xbox, PlayStation, and more. 74 | - **Architectural support**: x86_64, aarch64, x86, ARM, and more. 75 | 76 | ## Getting Started 77 | 78 | To get started with RustyDLNA, follow these steps: 79 | 80 | 1. **Clone the repository**: 81 | 82 | First, clone the repository to your local machine: 83 | 84 | ```bash 85 | git clone https://github.com/pj1234678/RustyDLNA.git 86 | cd RustyDLNA 87 | ``` 88 | 89 | 2. **Configure the IP address and path**: 90 | 91 | Open the `src/main.rs` file and modify the server's IP address and the path it serves. Find the relevant variables and update them to your desired values: 92 | 93 | ```rust 94 | const IP_ADDRESS: &str = "192.168.2.220"; 95 | const DIR_PATH: &str = "./"; 96 | ``` 97 | 98 | 3. **Run the server**: 99 | 100 | After configuring the server, you can start it by running the following command: 101 | 102 | ```bash 103 | cargo run 104 | ``` 105 | 106 | ## Contributing 107 | 108 | We welcome contributions to RustyDLNA! If you're interested in helping out, please review our [contributing guidelines](CONTRIBUTING.md) and submit a pull request. 109 | 110 | ## License 111 | 112 | RustyDLNA is licensed under the **MIT** license. 113 | 114 | --- 115 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::net::TcpListener; 2 | use std::net::UdpSocket; 3 | use std::thread; 4 | use std::sync::mpsc; 5 | use std::net::TcpStream; 6 | use std::sync::Arc; 7 | use std::sync::Mutex; 8 | use std::io::Write; 9 | use std::io::Read; 10 | use std::fs; 11 | use std::io::SeekFrom; 12 | use std::path::Path; 13 | 14 | use std::fs::File; 15 | use std::collections::BTreeMap; 16 | use std::io::Seek; 17 | use std::collections::HashMap; 18 | 19 | use std::time::Duration; 20 | const IP_ADDRESS: &str = "192.168.2.220"; 21 | const DIR_PATH: &str = "./"; 22 | const NUM_THREADS: i32 = 256; // Number of threads in the thread pool 23 | 24 | 25 | fn main() { 26 | let cache: Arc>>> = Arc::new(Mutex::new(HashMap::new())); 27 | let tcp_listener = TcpListener::bind("0.0.0.0:8200").unwrap(); 28 | println!("DLNA server listening on port 8200"); 29 | 30 | let ssdp_socket = UdpSocket::bind("0.0.0.0:1900").unwrap(); 31 | let multicast_addr = "239.255.255.250".parse().unwrap(); 32 | ssdp_socket.join_multicast_v4(&multicast_addr, &IP_ADDRESS.parse().unwrap()).unwrap(); 33 | let mut response_bytes = Vec::new(); 34 | 35 | write!( 36 | response_bytes, 37 | "HTTP/1.1 200 OK\r\n\ 38 | CACHE-CONTROL: max-age=1800\r\n\ 39 | EXT:\r\n\ 40 | LOCATION: http://{}:8200/rootDesc.xml\r\n\ 41 | SERVER: DLNA/1.0 DLNADOC/1.50 UPnP/1.0 RustyDLNA6/1.3.0\r\n\ 42 | ST: urn:schemas-upnp-org:device:MediaServer:1\r\n\ 43 | USN: uuid:4d696e69-444c-164e-9d41-b827eb96c6c2::urn:schemas-upnp-org:device:MediaServer:1\r\n\ 44 | \r\n", 45 | IP_ADDRESS 46 | ).unwrap(); 47 | let mut buffer = [0; 4096]; 48 | 49 | thread::spawn(move || { 50 | loop { 51 | match ssdp_socket.recv_from(&mut buffer) { 52 | Ok((_size, src_addr)) => { 53 | println!("Received SSDP search request from: {:?} Size: {}", src_addr, buffer.len()); 54 | match ssdp_socket.send_to(&response_bytes, src_addr) { 55 | Err(err) => eprintln!("Failed to send SSDP response: {:?}", err), 56 | Ok(_) => println!("Sent SSDP response to: {:?}", src_addr), 57 | } 58 | } 59 | Err(err) => eprintln!("Failed to receive SSDP request: {:?}", err), 60 | } 61 | } 62 | }); 63 | 64 | // Create a channel for communication between the main thread and worker threads 65 | let (tx, rx) = mpsc::channel(); 66 | let rx = Arc::new(Mutex::new(rx)); 67 | 68 | // Spawn worker threads 69 | for _ in 0..NUM_THREADS { 70 | let rx = Arc::clone(&rx); 71 | let cache = Arc::clone(&cache); 72 | thread::spawn(move || { 73 | loop { 74 | let stream = rx.lock().unwrap().recv().unwrap(); 75 | // Handle each TCP connection 76 | handle_client(stream, cache.clone()); 77 | } 78 | }); 79 | } 80 | 81 | // Main loop for handling TCP connections 82 | for tcp_stream in tcp_listener.incoming() { 83 | match tcp_stream { 84 | Ok(stream) => { 85 | tx.send(stream).unwrap(); 86 | } 87 | Err(e) => { 88 | eprintln!("DLNA server error: {}", e); 89 | } 90 | } 91 | } 92 | } 93 | 94 | fn handle_client(mut stream: TcpStream, cache: Arc>>>) { 95 | 96 | let mut buffer = Vec::new(); 97 | let _ = stream.set_read_timeout(Some(Duration::from_millis(5000))); 98 | let _ = stream.set_write_timeout(Some(Duration::from_millis(5000))); 99 | 100 | loop { 101 | let mut buf = vec![0; 4096]; // Temporary buffer for each read operation 102 | match stream.read(&mut buf) { 103 | Ok(0) => { 104 | // End of stream (EOF) reached, break out of the loop 105 | break; 106 | }, 107 | Ok(n) => { 108 | // Data read successfully, extend buffer with the actual data read 109 | buffer.extend_from_slice(&buf[..n]); 110 | match n < buf.len() { 111 | true => { 112 | // Less than a full buffer read, so we're done 113 | break; 114 | }, 115 | false => (), 116 | } 117 | }, 118 | Err(e) => { 119 | match e.kind() { 120 | std::io::ErrorKind::WouldBlock => { 121 | // Non-blocking operation would block, continue looping or take other action 122 | // Continue looping or take appropriate action depending on your application logic 123 | // In some cases, you might want to sleep or wait before attempting to read again 124 | }, 125 | _ => { 126 | // Error occurred during read operation, break out of the loop or handle the error 127 | break; 128 | } 129 | } 130 | } 131 | } 132 | } 133 | 134 | match buffer.is_empty() { 135 | true => (), 136 | false => match std::str::from_utf8(&buffer) { 137 | Ok(request) => match request.split_whitespace().next() { 138 | Some(method) => match method.to_uppercase().as_str() { 139 | "GET" => handle_get_request(stream, request), 140 | "HEAD" => handle_head_request(stream), 141 | "POST" => handle_post_request(stream, request.to_string(), cache), 142 | _ => eprintln!("Unsupported HTTP method: {}", method), 143 | }, 144 | None => eprintln!("Malformed HTTP request: missing method"), 145 | }, 146 | Err(err) => eprintln!("Error decoding HTTP request: {}", err), 147 | }, 148 | } 149 | 150 | } 151 | 152 | 153 | 154 | fn handle_head_request(mut stream: TcpStream) { 155 | let response = "HTTP/1.1 200 OK\r\n"; 156 | let content_type = "Content-Type: video/mp4\r\n"; 157 | let content_length = format!("Content-Length: 9999\r\n"); 158 | let date_header = "Date: Fri, 08 Nov 2024 05:39:08 GMT\r\n"; 159 | let ext_header = "EXT:\r\n\r\n"; 160 | 161 | let _ = stream.write_all(format!("{}{}{}{}{}", response, content_type, content_length, date_header, ext_header).as_bytes()); 162 | 163 | } 164 | 165 | 166 | 167 | 168 | fn handle_get_request(mut stream: TcpStream, http_request: &str) { 169 | let mut http_request_parts = http_request.split_whitespace(); 170 | match http_request_parts.next() { 171 | Some(method) => method, 172 | None => { 173 | eprintln!("Malformed HTTP request: missing method"); 174 | return; 175 | } 176 | }; 177 | let http_path = match http_request_parts.next() { 178 | Some(path) => path, 179 | None => { 180 | eprintln!("Malformed HTTP request: missing path"); 181 | return; 182 | } 183 | }; 184 | let decoded_path = decode(http_path); 185 | let sanitized_path = sanitize_path(decoded_path); 186 | 187 | let combined_path = format!("{}/{}", DIR_PATH, sanitized_path); 188 | 189 | let mut file = match sanitized_path.as_str() { 190 | "icons/lrg.png" => { 191 | match File::open("lrg.png") { 192 | Ok(file) => file, 193 | Err(_) => { 194 | let response = "HTTP/1.1 404 NOT FOUND\r\n\r\n"; 195 | match stream.write_all(response.as_bytes()) { 196 | Ok(_) => return, 197 | Err(err) => { 198 | eprintln!("Error sending response: {}", err); 199 | return; 200 | } 201 | } 202 | } 203 | } 204 | } 205 | "ContentDir.xml" => { 206 | let xml_content = "10GetSearchCapabilitiesSearchCapsoutSearchCapabilitiesGetSortCapabilitiesSortCapsoutSortCapabilitiesGetSystemUpdateIDIdoutSystemUpdateIDBrowseObjectIDinA_ARG_TYPE_ObjectIDBrowseFlaginA_ARG_TYPE_BrowseFlagFilterinA_ARG_TYPE_FilterStartingIndexinA_ARG_TYPE_IndexRequestedCountinA_ARG_TYPE_CountSortCriteriainA_ARG_TYPE_SortCriteriaResultoutA_ARG_TYPE_ResultNumberReturnedoutA_ARG_TYPE_CountTotalMatchesoutA_ARG_TYPE_CountUpdateIDoutA_ARG_TYPE_UpdateIDSearchContainerIDinA_ARG_TYPE_ObjectIDSearchCriteriainA_ARG_TYPE_SearchCriteriaFilterinA_ARG_TYPE_FilterStartingIndexinA_ARG_TYPE_IndexRequestedCountinA_ARG_TYPE_CountSortCriteriainA_ARG_TYPE_SortCriteriaResultoutA_ARG_TYPE_ResultNumberReturnedoutA_ARG_TYPE_CountTotalMatchesoutA_ARG_TYPE_CountUpdateIDoutA_ARG_TYPE_UpdateIDUpdateObjectObjectIDinA_ARG_TYPE_ObjectIDCurrentTagValueinA_ARG_TYPE_TagValueListNewTagValueinA_ARG_TYPE_TagValueListTransferIDsstringA_ARG_TYPE_ObjectIDstringA_ARG_TYPE_ResultstringA_ARG_TYPE_SearchCriteriastringA_ARG_TYPE_BrowseFlagstringBrowseMetadataBrowseDirectChildrenA_ARG_TYPE_FilterstringA_ARG_TYPE_SortCriteriastringA_ARG_TYPE_Indexui4A_ARG_TYPE_Countui4A_ARG_TYPE_UpdateIDui4A_ARG_TYPE_TagValueListstringSearchCapabilitiesstringSortCapabilitiesstringSystemUpdateIDui4"; 207 | let mut response = Vec::new(); 208 | 209 | write!( 210 | response, 211 | "HTTP/1.1 200 OK\r\n\ 212 | Content-Length: {}\r\n\ 213 | Content-Type: text/xml\r\n\ 214 | \r\n\ 215 | {}", 216 | xml_content.len(), 217 | xml_content 218 | ).unwrap(); 219 | 220 | match stream.write_all(response.as_slice()) { 221 | Ok(_) => return, 222 | Err(err) => { 223 | eprintln!("Error sending response: {}", err); 224 | return; 225 | } 226 | } 227 | } 228 | "X_MS_MediaReceiverRegistrar.xml" => { 229 | let xml_content = "10IsAuthorizedDeviceIDinA_ARG_TYPE_DeviceIDResultoutA_ARG_TYPE_ResultIsValidatedDeviceIDinA_ARG_TYPE_DeviceIDResultoutA_ARG_TYPE_ResultRegisterDeviceRegistrationReqMsginA_ARG_TYPE_RegistrationReqMsgRegistrationRespMsgoutA_ARG_TYPE_RegistrationRespMsgA_ARG_TYPE_DeviceIDstringA_ARG_TYPE_RegistrationReqMsgbin.base64A_ARG_TYPE_RegistrationRespMsgbin.base64A_ARG_TYPE_ResultintAuthorizationDeniedUpdateIDui4AuthorizationGrantedUpdateIDui4ValidationRevokedUpdateIDui4ValidationSucceededUpdateIDui4"; 230 | 231 | let mut response = Vec::new(); 232 | 233 | write!( 234 | response, 235 | "HTTP/1.1 200 OK\r\n\ 236 | Content-Length: {}\r\n\ 237 | Content-Type: text/xml\r\n\ 238 | \r\n\ 239 | {}", 240 | xml_content.len(), 241 | xml_content 242 | ).unwrap(); 243 | 244 | match stream.write_all(response.as_slice()) { 245 | Ok(_) => return, 246 | Err(err) => { 247 | eprintln!("Error sending response: {}", err); 248 | return; 249 | } 250 | } 251 | } 252 | "ConnectionMgr.xml" => { 253 | let xml_content = "10GetProtocolInfoSourceoutSourceProtocolInfoSinkoutSinkProtocolInfoGetCurrentConnectionIDsConnectionIDsoutCurrentConnectionIDsGetCurrentConnectionInfoConnectionIDinA_ARG_TYPE_ConnectionIDRcsIDoutA_ARG_TYPE_RcsIDAVTransportIDoutA_ARG_TYPE_AVTransportIDProtocolInfooutA_ARG_TYPE_ProtocolInfoPeerConnectionManageroutA_ARG_TYPE_ConnectionManagerPeerConnectionIDoutA_ARG_TYPE_ConnectionIDDirectionoutA_ARG_TYPE_DirectionStatusoutA_ARG_TYPE_ConnectionStatusSourceProtocolInfostringSinkProtocolInfostringCurrentConnectionIDsstringA_ARG_TYPE_ConnectionStatusstringOKContentFormatMismatchInsufficientBandwidthUnreliableChannelUnknownA_ARG_TYPE_ConnectionManagerstringA_ARG_TYPE_DirectionstringInputOutputA_ARG_TYPE_ProtocolInfostringA_ARG_TYPE_ConnectionIDi4A_ARG_TYPE_AVTransportIDi4A_ARG_TYPE_RcsIDi4"; 254 | let mut response = Vec::new(); 255 | 256 | write!( 257 | response, 258 | "HTTP/1.1 200 OK\r\n\ 259 | Content-Length: {}\r\n\ 260 | Content-Type: text/xml\r\n\ 261 | \r\n\ 262 | {}", 263 | xml_content.len(), 264 | xml_content 265 | ).unwrap(); 266 | 267 | match stream.write_all(response.as_slice()) { 268 | Ok(_) => return, 269 | Err(err) => { 270 | eprintln!("Error sending response: {}", err); 271 | return; 272 | } 273 | } 274 | } 275 | "rootDesc.xml" => { 276 | let xml_content = "\r\n10urn:schemas-upnp-org:device:MediaServer:1RustyDLNA6RustyDLNA6http://www.netgear.com/RustyDLNA on LinuxWindows Media Connect compatible (MiniDLNA)1.3.0http://www.netgear.com00000000uuid:4d696e69-444c-164e-9d41-b827eb96c6c2DMS-1.50/image/png484824/icons/sm.pngimage/png12012024/icons/lrg.pngimage/jpeg484824/icons/sm.jpgimage/jpeg12012024/icons/lrg.jpgurn:schemas-upnp-org:service:ContentDirectory:1urn:upnp-org:serviceId:ContentDirectory/ctl/ContentDir/evt/ContentDir/ContentDir.xmlurn:schemas-upnp-org:service:ConnectionManager:1urn:upnp-org:serviceId:ConnectionManager/ctl/ConnectionMgr/evt/ConnectionMgr/ConnectionMgr.xmlurn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar/ctl/X_MS_MediaReceiverRegistrar/evt/X_MS_MediaReceiverRegistrar/X_MS_MediaReceiverRegistrar.xml"; 277 | let mut response = Vec::new(); 278 | 279 | write!( 280 | response, 281 | "HTTP/1.1 200 OK\r\n\ 282 | Content-Length: {}\r\n\ 283 | Content-Type: text/xml\r\n\ 284 | \r\n\ 285 | {}", 286 | xml_content.len(), 287 | xml_content 288 | ).unwrap(); 289 | 290 | match stream.write_all(response.as_slice()) { 291 | Ok(_) => return, 292 | Err(err) => { 293 | eprintln!("Error sending response: {}", err); 294 | return; 295 | } 296 | } 297 | } 298 | _ => match File::open(&combined_path) { 299 | Ok(file) => file, 300 | Err(err) => { 301 | eprintln!("Error opening file: {}, Reason: {}", combined_path, err); 302 | return; 303 | } 304 | }, 305 | }; 306 | 307 | // Extracting Range header 308 | let mut range: u64 = 0; 309 | match http_request.lines().find(|line| line.starts_with("Range: bytes=")) { 310 | Some(line) => { 311 | match line.strip_prefix("Range: bytes=") { 312 | Some(r) => { 313 | match r.split('-').next().and_then(|num| num.parse::().ok()) { 314 | Some(parsed_range) => { 315 | range = parsed_range; 316 | } 317 | None => println!("Failed to parse range value"), 318 | } 319 | } 320 | None => println!("Failed to strip prefix from Range header"), 321 | } 322 | } 323 | None => println!("No Range header found"), 324 | } 325 | 326 | let file_size = file.metadata().unwrap().len(); 327 | 328 | file.seek(SeekFrom::Start(range)).unwrap(); 329 | 330 | let mut response_header = Vec::new(); 331 | 332 | write!( 333 | response_header, 334 | "HTTP/1.1 206 Partial Content\r\n\ 335 | Content-Range: bytes {}-{}/{}\r\n\ 336 | Content-Type: video/mp4\r\n\ 337 | Content-Length: {}\r\n\ 338 | \r\n", 339 | range, 340 | file_size - 1, 341 | file_size, 342 | file_size - range, 343 | ).unwrap(); 344 | 345 | match stream.write(&response_header) { 346 | Ok(_) => (), 347 | Err(err) => { 348 | eprintln!("Error sending response header: {}", err); 349 | return; 350 | } 351 | } 352 | 353 | let mut buffer = [0; 8192]; 354 | let mut remaining = file_size - range; 355 | 356 | while remaining > 0 { 357 | let bytes_to_read = std::cmp::min(remaining as usize, buffer.len()); 358 | let bytes_read = match file.read(&mut buffer[..bytes_to_read]) { 359 | Ok(0) => break, 360 | Ok(bytes_read) => bytes_read, 361 | Err(err) => { 362 | eprintln!("Error reading file: {}", err); 363 | return; 364 | } 365 | }; 366 | 367 | match stream.write_all(&buffer[..bytes_read]) { 368 | Ok(_) => (), 369 | Err(err) => { 370 | eprintln!("Error sending response body: {}", err); 371 | return; 372 | } 373 | } 374 | 375 | remaining -= bytes_read as u64; 376 | } 377 | } 378 | 379 | 380 | fn handle_post_request( 381 | mut stream: TcpStream, 382 | request: String, 383 | cache: Arc>>>, 384 | ) { 385 | println!("Request: {}", request); 386 | 387 | let contains_get_sort_capabilities = request.contains("#GetSortCapabilities"); 388 | let xml_content = "dc:title,dc:date,upnp:class,upnp:album,upnp:episodeNumber,upnp:originalTrackNumber"; 389 | 390 | let mut response = Vec::new(); 391 | write!( 392 | &mut response, 393 | "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nContent-Type: text/xml\r\n\r\n{}", 394 | xml_content.len(), 395 | xml_content 396 | ).unwrap(); 397 | 398 | match contains_get_sort_capabilities { 399 | true => match stream.write_all(&response) { 400 | Err(err) => eprintln!("Error sending response: {}", err), 401 | _ => return, 402 | }, 403 | false => (), 404 | } 405 | 406 | // Extract the ObjectID (existing logic) 407 | let object_id = request 408 | .find("ObjectID") 409 | .and_then(|start_index| { 410 | request[start_index..] 411 | .find('>') 412 | .map(|open_index| start_index + open_index + 1) 413 | }) 414 | .and_then(|object_id_start| { 415 | request[object_id_start..] 416 | .find('<') 417 | .map(|end_index| &request[object_id_start..object_id_start + end_index]) 418 | }) 419 | .unwrap_or(""); 420 | println!("Object ID: {}", object_id); 421 | 422 | // Extract the User-Agent (new logic) 423 | let user_agent = request 424 | .lines() 425 | .find(|line| line.to_lowercase().starts_with("user-agent:")) 426 | .and_then(|line| line.splitn(2, ':').nth(1)) 427 | .map(|agent| agent.trim().to_string()) 428 | .unwrap_or_else(|| "Unknown".to_string()); // Default to "Unknown" if User-Agent is not found 429 | 430 | println!("User-Agent: {}", user_agent); 431 | 432 | // Set requested_count to 5000 if the User-Agent matches the specified value 433 | let mut requested_count = request 434 | .find("") 435 | .and_then(|tmp| { 436 | request[..tmp] 437 | .rfind('>') 438 | .map(|tmp2| request[tmp2 + 1..tmp].trim()) 439 | }) 440 | .and_then(|value_str| value_str.parse::().ok()) 441 | .unwrap_or(0); // Default to 0 if not found 442 | 443 | match user_agent.contains("Platinum") { 444 | true => { 445 | requested_count = 5000; 446 | println!("User-Agent contains 'Platinum'. Requested count set to 5000."); 447 | } 448 | false => { 449 | println!("User-Agent does not contain 'Platinum'. Using requested_count: {}", requested_count); 450 | } 451 | } 452 | // Extract StartingIndex (existing logic) 453 | let starting_index = request 454 | .find("") 455 | .and_then(|start_index| { 456 | request[..start_index] 457 | .rfind('>') 458 | .map(|close_index| request[close_index + 1..start_index].trim()) 459 | }) 460 | .and_then(|value_str| value_str.parse::().ok()); 461 | 462 | let mut cache = match cache.lock() { 463 | Ok(locked_cache) => locked_cache, 464 | Err(_) => { 465 | eprintln!("Mutex poisoned. Could not acquire lock."); 466 | return; // Or handle as needed 467 | } 468 | }; 469 | 470 | // Get the cached response from the HashMap 471 | let cached_response = cache.get(object_id); 472 | match cached_response { 473 | Some(cached_response) => { 474 | let _ = stream.write_all(cached_response).map_err(|err| eprintln!("Error sending response: {}", err)); 475 | return; 476 | } 477 | None => { 478 | match object_id.is_empty() { 479 | true => { 480 | eprintln!("Error: ObjectID is empty."); 481 | return; // Return early if object_id is empty 482 | }, 483 | false => { 484 | // Continue with the rest of the logic if object_id is not empty 485 | let _ = object_id 486 | .strip_prefix("64$") 487 | .unwrap_or(object_id) 488 | .strip_prefix("0") 489 | .unwrap_or(object_id); 490 | 491 | // You can continue processing the object_id_stripped here... 492 | } 493 | } 494 | let object_id_stripped = object_id.strip_prefix("64$").unwrap_or(object_id).strip_prefix("0").unwrap_or(object_id); 495 | let combined_path = format!("{}/{}", DIR_PATH, &decode(object_id_stripped)); 496 | println!("Path Requested: {}", combined_path); 497 | println!("ObjectID Requested: {}", object_id_stripped); 498 | 499 | let path = Path::new(&combined_path); 500 | 501 | // Check if the object_id is a folder or a file 502 | if path.is_dir() { 503 | // If it's a folder, call generate_browse_response. 504 | let browse_response = generate_browse_response( 505 | object_id_stripped, 506 | &starting_index.unwrap(), 507 | &requested_count, // Use the updated requested_count here 508 | ); 509 | let response_bytes = browse_response.as_bytes(); // Convert the browse response to bytes. 510 | 511 | // Cache the response. 512 | cache.insert(object_id.to_string(), response_bytes.to_vec()); 513 | println!("Added ObjectID {} (folder) to cache.", object_id); 514 | 515 | // Write the response to the stream. 516 | let _ = stream.write_all(response_bytes).map_err(|err| eprintln!("Error sending response: {}", err)); 517 | return; 518 | } else if path.is_file() { 519 | println!("It's a file {}", path.display()); 520 | // If it's a file, call generate_meta. 521 | let meta_response = generate_meta_response(object_id); 522 | let response_bytes = meta_response.as_bytes(); // Convert the metadata response to bytes. 523 | 524 | // Write the response to the stream. 525 | let _ = stream.write_all(response_bytes).map_err(|err| eprintln!("Error sending response: {}", err)); 526 | return; 527 | } else { 528 | // Handle the case where the object is neither a folder nor a file (e.g., symbolic link, invalid path, etc.). 529 | eprintln!("Error: ObjectID {} is neither a valid file nor a valid folder.", object_id); 530 | return; // You could handle this differently, such as returning an error response. 531 | } 532 | } 533 | } 534 | } 535 | 536 | 537 | 538 | fn generate_meta_response(path: &str) -> String { 539 | // Hardcoded Date header and XML content as specified. 540 | let date_header = "Fri, 08 Nov 2024 05:39:08 GMT"; 541 | let result_xml = format!( 542 | r#"<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"><item id="64$0" parentID="64" restricted="1"><dc:title></dc:title><upnp:class>object.item.videoItem</upnp:class><dc:date>2024-11-07T21:38:51</dc:date><upnp:playbackCount>0</upnp:playbackCount><res size="21397012" duration="0:01:00.019" resolution="3840x2160" protocolInfo="http-get:*:video/mp4:DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000">http://{}:8200/{}</res></item></DIDL-Lite>"#, 543 | IP_ADDRESS, 544 | path 545 | ); 546 | println!("{}", result_xml); 547 | // Concatenate all parts into a single string. 548 | let response = format!( 549 | "HTTP/1.1 200 OK\r\nContent-Type: text/xml; charset=\"utf-8\"\r\nConnection: close\r\nContent-Length: 2048\r\nServer: Debian DLNADOC/1.50 UPnP/1.0 MiniDLNA/1.3.0\r\nDate: {}\r\nEXT:\r\n\r\n\r\n{}111", 550 | date_header, 551 | result_xml 552 | ); 553 | 554 | response 555 | } 556 | 557 | fn generate_browse_response(path: &str, starting_index: &u32, requested_count: &u32) -> String { 558 | 559 | let combined_path = format!("{}/{}", DIR_PATH, &decode(path)); 560 | let mut soap_response = String::with_capacity(1024); 561 | let mut count = 0; 562 | 563 | soap_response.push_str("<DIDL-Lite xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\">"); 564 | 565 | let mut directories = BTreeMap::new(); 566 | let mut files = BTreeMap::new(); 567 | 568 | match fs::read_dir(combined_path.clone()) { 569 | Ok(dir_entries) => { 570 | for entry in dir_entries.filter_map(Result::ok) { 571 | match entry.file_name().to_str() { 572 | Some(name) => { 573 | let entry_path = entry.path(); 574 | let is_dir = entry_path.is_dir(); 575 | match is_dir { 576 | true => { 577 | directories.insert(name.to_string(), entry_path); 578 | } 579 | false => { 580 | files.insert(name.to_string(), entry_path); 581 | } 582 | }; 583 | } 584 | None => println!("Failed to convert entry name to string"), 585 | } 586 | } 587 | } 588 | Err(_) => println!("Error reading directory: {}", combined_path), 589 | } 590 | 591 | let mut loop_count = 0; 592 | // Process directories first 593 | for (name, _) in directories { 594 | match loop_count >= *starting_index + requested_count { 595 | true => break, 596 | false => (), 597 | } 598 | match loop_count < *starting_index { 599 | true => { 600 | loop_count += 1; 601 | continue; 602 | } 603 | false => (), 604 | } 605 | 606 | soap_response += &format!( 607 | "<container id=\"{}{}/\" parentID=\"{}/\" restricted=\"1\" searchable=\"1\" childCount=\"0\"><dc:title>{}</dc:title><upnp:class>object.container.storageFolder</upnp:class><upnp:storageUsed>-1</upnp:storageUsed></container>", 608 | path, encode_title_name(&name), path, encode_title_name(&name) 609 | ); 610 | println!( 611 | "<container id=\"{}{}/\" parentID=\"{}/\" restricted=\"1\" searchable=\"1\" childCount=\"0\"><dc:title>{}</dc:title><upnp:class>object.container.storageFolder</upnp:class><upnp:storageUsed>-1</upnp:storageUsed></container>", 612 | path, encode_title_name(&name), path, encode_title_name(&name) 613 | ); 614 | 615 | loop_count += 1; 616 | count += 1; 617 | } 618 | 619 | // Process files 620 | for (name, _) in files { 621 | match loop_count >= *starting_index + requested_count { 622 | true => break, 623 | false => (), 624 | } 625 | match loop_count < *starting_index { 626 | true => { 627 | loop_count += 1; 628 | continue; 629 | } 630 | false => (), 631 | } 632 | 633 | soap_response += &format!( 634 | "<item id=\"{}{}\" parentID=\"{}\" restricted=\"1\" searchable=\"1\"><dc:title>{}</dc:title><upnp:class>object.item.videoItem</upnp:class><res protocolInfo=\"http-get:*:video/mp4:*\">http://{}:8200/{}{}</res></item>", 635 | path, encode(&name), encode(path), encode_title_name(&name), IP_ADDRESS, encode(path), encode(&name) 636 | ); 637 | 638 | 639 | loop_count += 1; 640 | count += 1; 641 | } 642 | 643 | // Append the closing tags using format! 644 | soap_response += &format!( 645 | "</DIDL-Lite>{}{}0", 646 | count, count 647 | ); 648 | 649 | let soap_response_size = soap_response.len(); 650 | format!("HTTP/1.1 200 OK\r\nConnection: Keep-Alive\r\nContent-Type: text/xml;\r\nContent-Length: {}\r\nServer: RustyDLNA DLNADOC/1.50 UPnP/1.0 RustyDLNA6/1.3.0\r\n\r\n{}", soap_response_size, soap_response) 651 | } 652 | 653 | fn sanitize_path(path: String) -> String { 654 | let mut parts: Vec<&str> = path.split('/').collect(); 655 | 656 | let mut i = 0; 657 | while i < parts.len() { 658 | match parts[i] { 659 | // ignore leading slashes, trailing slashes, duplicate slashes 660 | // and single dot dirs 661 | "" | "." => { 662 | parts.remove(i); 663 | }, 664 | ".." => { 665 | parts.remove(i); 666 | 667 | // go up one dir (if possible) 668 | if i > 0 { 669 | parts.remove(i - 1); 670 | i -= 1; 671 | } 672 | }, 673 | _ => { 674 | i += 1; 675 | } 676 | } 677 | } 678 | 679 | return parts.join("/"); 680 | } 681 | 682 | fn decode(s: &str) -> String { 683 | let mut decoded = String::from(s); 684 | decoded = decoded.replace("%20", " "); 685 | decoded = decoded.replace("%27", "'"); 686 | decoded = decoded.replace("%28", "("); 687 | decoded = decoded.replace("%29", ")"); 688 | decoded = decoded.replace("%22", "\""); 689 | decoded = decoded.replace("%23", "#"); 690 | decoded = decoded.replace("%2C", ","); 691 | decoded = decoded.replace("%E2%80%99", "\u{2019}"); 692 | decoded = decoded.replace("'", "'"); 693 | decoded = decoded.replace("&", "&"); 694 | decoded = decoded.replace("&amp;", "&"); 695 | decoded = decoded.replace("%C3%A1", "\u{00E1}"); 696 | decoded = decoded.replace("%C3%A9", "\u{00E9}"); 697 | decoded 698 | } 699 | 700 | 701 | fn encode(s: &str) -> String { 702 | let mut encoded = String::from(s); 703 | 704 | encoded = encoded.replace(' ', "%20"); 705 | encoded = encoded.replace('\'', "%27"); 706 | encoded = encoded.replace('(', "%28"); 707 | encoded = encoded.replace(')', "%29"); 708 | encoded = encoded.replace('\"', "%22"); 709 | encoded = encoded.replace('#', "%23"); 710 | encoded = encoded.replace(',', "%2C"); 711 | encoded = encoded.replace('\u{2019}', "%E2%80%99"); 712 | encoded = encoded.replace('&', "&amp;"); 713 | encoded = encoded.replace('\u{00E1}', "%C3%A1"); 714 | encoded = encoded.replace('\u{00E9}', "%C3%A9"); 715 | encoded 716 | } 717 | fn encode_title_name(s: &str) -> String { 718 | let mut encoded = String::from(s); 719 | 720 | encoded = encoded.replace('&', "&amp;"); 721 | encoded 722 | } 723 | --------------------------------------------------------------------------------