|
| 1 | +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy |
| 2 | +// SPDX-License-Identifier: Apache-2.0 |
| 3 | +// SPDX-License-Identifier: MIT |
| 4 | + |
| 5 | +#![cfg(protocol_asset)] |
| 6 | + |
| 7 | +use crate::api::file::SafePathBuf; |
| 8 | +use crate::scope::FsScope; |
| 9 | +use rand::RngCore; |
| 10 | +use std::io::SeekFrom; |
| 11 | +use tauri_runtime::http::HttpRange; |
| 12 | +use tauri_runtime::http::{ |
| 13 | + header::*, status::StatusCode, MimeType, Request, Response, ResponseBuilder, |
| 14 | +}; |
| 15 | +use tauri_utils::debug_eprintln; |
| 16 | +use tokio::fs::File; |
| 17 | +use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; |
| 18 | +use url::Position; |
| 19 | +use url::Url; |
| 20 | + |
| 21 | +pub fn asset_protocol_handler( |
| 22 | + request: &Request, |
| 23 | + scope: FsScope, |
| 24 | + window_origin: String, |
| 25 | +) -> Result<Response, Box<dyn std::error::Error>> { |
| 26 | + let parsed_path = Url::parse(request.uri())?; |
| 27 | + let filtered_path = &parsed_path[..Position::AfterPath]; |
| 28 | + let path = filtered_path |
| 29 | + .strip_prefix("asset://localhost/") |
| 30 | + // the `strip_prefix` only returns None when a request is made to `https://tauri.$P` on Windows |
| 31 | + // where `$P` is not `localhost/*` |
| 32 | + .unwrap_or(""); |
| 33 | + let path = percent_encoding::percent_decode(path.as_bytes()) |
| 34 | + .decode_utf8_lossy() |
| 35 | + .to_string(); |
| 36 | + |
| 37 | + if let Err(e) = SafePathBuf::new(path.clone().into()) { |
| 38 | + debug_eprintln!("asset protocol path \"{}\" is not valid: {}", path, e); |
| 39 | + return ResponseBuilder::new().status(403).body(Vec::new()); |
| 40 | + } |
| 41 | + |
| 42 | + if !scope.is_allowed(&path) { |
| 43 | + debug_eprintln!("asset protocol not configured to allow the path: {}", path); |
| 44 | + return ResponseBuilder::new().status(403).body(Vec::new()); |
| 45 | + } |
| 46 | + |
| 47 | + let mut resp = ResponseBuilder::new().header("Access-Control-Allow-Origin", &window_origin); |
| 48 | + |
| 49 | + crate::async_runtime::block_on(async move { |
| 50 | + let mut file = File::open(&path).await?; |
| 51 | + |
| 52 | + // get file length |
| 53 | + let len = { |
| 54 | + let old_pos = file.stream_position().await?; |
| 55 | + let len = file.seek(SeekFrom::End(0)).await?; |
| 56 | + file.seek(SeekFrom::Start(old_pos)).await?; |
| 57 | + len |
| 58 | + }; |
| 59 | + |
| 60 | + // get file mime type |
| 61 | + let (mime_type, read_bytes) = { |
| 62 | + let nbytes = len.min(8192); |
| 63 | + let mut magic_buf = Vec::with_capacity(nbytes as usize); |
| 64 | + let old_pos = file.stream_position().await?; |
| 65 | + (&mut file).take(nbytes).read_to_end(&mut magic_buf).await?; |
| 66 | + file.seek(SeekFrom::Start(old_pos)).await?; |
| 67 | + ( |
| 68 | + MimeType::parse(&magic_buf, &path), |
| 69 | + // return the `magic_bytes` if we read the whole file |
| 70 | + // to avoid reading it again later if this is not a range request |
| 71 | + if len < 8192 { Some(magic_buf) } else { None }, |
| 72 | + ) |
| 73 | + }; |
| 74 | + |
| 75 | + resp = resp.header(CONTENT_TYPE, &mime_type); |
| 76 | + |
| 77 | + // handle 206 (partial range) http requests |
| 78 | + let response = if let Some(range_header) = request |
| 79 | + .headers() |
| 80 | + .get("range") |
| 81 | + .and_then(|r| r.to_str().map(|r| r.to_string()).ok()) |
| 82 | + { |
| 83 | + resp = resp.header(ACCEPT_RANGES, "bytes"); |
| 84 | + |
| 85 | + let not_satisfiable = || { |
| 86 | + ResponseBuilder::new() |
| 87 | + .status(StatusCode::RANGE_NOT_SATISFIABLE) |
| 88 | + .header(CONTENT_RANGE, format!("bytes */{len}")) |
| 89 | + .body(vec![]) |
| 90 | + }; |
| 91 | + |
| 92 | + // parse range header |
| 93 | + let ranges = if let Ok(ranges) = HttpRange::parse(&range_header, len) { |
| 94 | + ranges |
| 95 | + .iter() |
| 96 | + // map the output to spec range <start-end>, example: 0-499 |
| 97 | + .map(|r| (r.start, r.start + r.length - 1)) |
| 98 | + .collect::<Vec<_>>() |
| 99 | + } else { |
| 100 | + return not_satisfiable(); |
| 101 | + }; |
| 102 | + |
| 103 | + /// The Maximum bytes we send in one range |
| 104 | + const MAX_LEN: u64 = 1000 * 1024; |
| 105 | + |
| 106 | + // single-part range header |
| 107 | + if ranges.len() == 1 { |
| 108 | + let &(start, mut end) = ranges.first().unwrap(); |
| 109 | + |
| 110 | + // check if a range is not satisfiable |
| 111 | + // |
| 112 | + // this should be already taken care of by the range parsing library |
| 113 | + // but checking here again for extra assurance |
| 114 | + if start >= len || end >= len || end < start { |
| 115 | + return not_satisfiable(); |
| 116 | + } |
| 117 | + |
| 118 | + // adjust end byte for MAX_LEN |
| 119 | + end = start + (end - start).min(len - start).min(MAX_LEN - 1); |
| 120 | + |
| 121 | + // calculate number of bytes needed to be read |
| 122 | + let nbytes = end + 1 - start; |
| 123 | + |
| 124 | + let mut buf = Vec::with_capacity(nbytes as usize); |
| 125 | + file.seek(SeekFrom::Start(start)).await?; |
| 126 | + file.take(nbytes).read_to_end(&mut buf).await?; |
| 127 | + |
| 128 | + resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}")); |
| 129 | + resp = resp.header(CONTENT_LENGTH, end + 1 - start); |
| 130 | + resp = resp.status(StatusCode::PARTIAL_CONTENT); |
| 131 | + resp.body(buf) |
| 132 | + } else { |
| 133 | + // multi-part range header |
| 134 | + let mut buf = Vec::new(); |
| 135 | + let ranges = ranges |
| 136 | + .iter() |
| 137 | + .filter_map(|&(start, mut end)| { |
| 138 | + // filter out unsatisfiable ranges |
| 139 | + // |
| 140 | + // this should be already taken care of by the range parsing library |
| 141 | + // but checking here again for extra assurance |
| 142 | + if start >= len || end >= len || end < start { |
| 143 | + None |
| 144 | + } else { |
| 145 | + // adjust end byte for MAX_LEN |
| 146 | + end = start + (end - start).min(len - start).min(MAX_LEN - 1); |
| 147 | + Some((start, end)) |
| 148 | + } |
| 149 | + }) |
| 150 | + .collect::<Vec<_>>(); |
| 151 | + |
| 152 | + let boundary = random_boundary(); |
| 153 | + let boundary_sep = format!("\r\n--{boundary}\r\n"); |
| 154 | + let boundary_closer = format!("\r\n--{boundary}\r\n"); |
| 155 | + |
| 156 | + resp = resp.header( |
| 157 | + CONTENT_TYPE, |
| 158 | + format!("multipart/byteranges; boundary={boundary}"), |
| 159 | + ); |
| 160 | + |
| 161 | + for (end, start) in ranges { |
| 162 | + // a new range is being written, write the range boundary |
| 163 | + buf.write_all(boundary_sep.as_bytes()).await?; |
| 164 | + |
| 165 | + // write the needed headers `Content-Type` and `Content-Range` |
| 166 | + buf |
| 167 | + .write_all(format!("{CONTENT_TYPE}: {mime_type}\r\n").as_bytes()) |
| 168 | + .await?; |
| 169 | + buf |
| 170 | + .write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes()) |
| 171 | + .await?; |
| 172 | + |
| 173 | + // write the separator to indicate the start of the range body |
| 174 | + buf.write_all("\r\n".as_bytes()).await?; |
| 175 | + |
| 176 | + // calculate number of bytes needed to be read |
| 177 | + let nbytes = end + 1 - start; |
| 178 | + |
| 179 | + let mut local_buf = Vec::with_capacity(nbytes as usize); |
| 180 | + file.seek(SeekFrom::Start(start)).await?; |
| 181 | + (&mut file).take(nbytes).read_to_end(&mut local_buf).await?; |
| 182 | + buf.extend_from_slice(&local_buf); |
| 183 | + } |
| 184 | + // all ranges have been written, write the closing boundary |
| 185 | + buf.write_all(boundary_closer.as_bytes()).await?; |
| 186 | + |
| 187 | + resp.body(buf) |
| 188 | + } |
| 189 | + } else { |
| 190 | + // avoid reading the file if we already read it |
| 191 | + // as part of mime type detection |
| 192 | + let buf = if let Some(b) = read_bytes { |
| 193 | + b |
| 194 | + } else { |
| 195 | + let mut local_buf = Vec::with_capacity(len as usize); |
| 196 | + file.read_to_end(&mut local_buf).await?; |
| 197 | + local_buf |
| 198 | + }; |
| 199 | + resp = resp.header(CONTENT_LENGTH, len); |
| 200 | + resp.body(buf) |
| 201 | + }; |
| 202 | + |
| 203 | + response |
| 204 | + }) |
| 205 | +} |
| 206 | + |
| 207 | +fn random_boundary() -> String { |
| 208 | + let mut x = [0_u8; 30]; |
| 209 | + rand::thread_rng().fill_bytes(&mut x); |
| 210 | + (x[..]) |
| 211 | + .iter() |
| 212 | + .map(|&x| format!("{x:x}")) |
| 213 | + .fold(String::new(), |mut a, x| { |
| 214 | + a.push_str(x.as_str()); |
| 215 | + a |
| 216 | + }) |
| 217 | +} |
0 commit comments