diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 2491789e6a4..036fd5bc724 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -39,7 +39,7 @@ env: # vcpkg version: 2025.08.27 # If we change the `VCPKG COMMIT_ID`, please remember: # 1. Call `$VCPKG_ROOT/vcpkg x-update-baseline` to update the baseline in `vcpkg.json`. - # Or we may face build issue like + # Or we may face build issue like # https://github.com/rustdesk/rustdesk/actions/runs/14414119794/job/40427970174 # 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`. VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b" @@ -49,6 +49,7 @@ env: #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" + MACOS_NOTARIZE_JSON: "${{ secrets.MACOS_NOTARIZE_JSON }}" UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}" SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}-2" @@ -615,7 +616,7 @@ jobs: run: | rustup target add ${{ matrix.job.target }} cargo build --locked --features flutter,hwcodec --release --target aarch64-apple-ios --lib - + - name: Upload liblibrustdesk.a Artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -693,12 +694,20 @@ jobs: - name: Check sign and import sign key if: env.MACOS_P12_BASE64 != null + env: + MACOS_CODESIGN_IDENTITY: ${{ secrets.MACOS_CODESIGN_IDENTITY }} + shell: bash run: | security default-keychain -s rustdesk.keychain security find-identity -v + if ! [[ "$MACOS_CODESIGN_IDENTITY" =~ ^[A-Za-z0-9]+$ ]]; then + # Ensure no whitespaces or special characters + echo 'FATAL: Invalid `secrets.MACOS_CODESIGN_IDENTITY` given. If signing key is stored on your Mac, you can run `security find-identity -v -p codesigning` to find out hex format of your identity.' >&2 + exit 128 + fi - name: Import notarize key - if: env.MACOS_P12_BASE64 != null + if: env.MACOS_P12_BASE64 != null && env.MACOS_NOTARIZE_JSON != null uses: timheuer/base64-to-file@adaa40c0c581f276132199d4cf60afa07ce60eac # v1.2 with: # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling @@ -846,8 +855,10 @@ jobs: codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv - # notarize the rustdesk-${{ env.VERSION }}.dmg - rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg + if [ "$MACOS_NOTARIZE_JSON" != "" ]; then + # notarize the rustdesk-${{ env.VERSION }}.dmg + rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg + fi - name: Rename rustdesk if: env.UPLOAD_ARTIFACT == 'true' diff --git a/src/hbbs_http.rs b/src/hbbs_http.rs index 9e4538697a2..e33c811d4e5 100644 --- a/src/hbbs_http.rs +++ b/src/hbbs_http.rs @@ -9,8 +9,8 @@ mod http_client; pub mod record_upload; pub mod sync; pub use http_client::{ - create_http_client_async, create_http_client_async_with_url, create_http_client_with_url, - get_url_for_tls, + create_http_client_async, create_http_client_async_with_url_strict, + create_http_client_with_url, create_http_client_with_url_strict, get_url_for_tls, }; #[derive(Debug)] diff --git a/src/hbbs_http/downloader.rs b/src/hbbs_http/downloader.rs index 573e7e77c4e..a4fd1fa3261 100644 --- a/src/hbbs_http/downloader.rs +++ b/src/hbbs_http/downloader.rs @@ -1,4 +1,4 @@ -use super::create_http_client_async_with_url; +use super::create_http_client_async_with_url_strict; use hbb_common::{ bail, lazy_static::lazy_static, @@ -167,7 +167,7 @@ async fn do_download( auto_del_dur: Option, mut rx_cancel: UnboundedReceiver<()>, ) -> ResultType { - let client = create_http_client_async_with_url(&url).await; + let client = create_http_client_async_with_url_strict(&url).await?; let mut is_all_downloaded = false; tokio::select! { diff --git a/src/hbbs_http/http_client.rs b/src/hbbs_http/http_client.rs index 432e5fa3869..4e34054927b 100644 --- a/src/hbbs_http/http_client.rs +++ b/src/hbbs_http/http_client.rs @@ -1,5 +1,6 @@ use hbb_common::{ async_recursion::async_recursion, + bail, config::{Config, Socks5Server}, log::{self, info}, proxy::{Proxy, ProxyScheme}, @@ -7,6 +8,7 @@ use hbb_common::{ get_cached_tls_accept_invalid_cert, get_cached_tls_type, is_plain, upsert_tls_cache, TlsType, }, + ResultType, }; use reqwest::{blocking::Client as SyncClient, Client as AsyncClient}; @@ -137,6 +139,32 @@ pub fn create_http_client_with_url(url: &str) -> SyncClient { ) } +pub fn create_http_client_with_url_strict(url: &str) -> ResultType { + let parsed_url = url::Url::parse(url)?; + if parsed_url.scheme() != "https" { + bail!("Strict HTTP client requires HTTPS: {}", url); + } + let proxy_conf = Config::get_socks(); + let tls_url = get_url_for_tls(url, &proxy_conf); + let cached_tls_type = get_cached_tls_type(tls_url); + let cached_danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url); + let can_reuse_cached_probe = + cached_tls_type.is_some() && cached_danger_accept_invalid_cert == Some(false); + let tls_type = if can_reuse_cached_probe { + cached_tls_type.unwrap_or(TlsType::Rustls) + } else { + TlsType::Rustls + }; + Ok(create_http_client_with_url_( + url, + tls_url, + tls_type, + can_reuse_cached_probe, + Some(false), + Some(false), + )) +} + fn create_http_client_with_url_( url: &str, tls_url: &str, @@ -247,6 +275,33 @@ pub async fn create_http_client_async_with_url(url: &str) -> AsyncClient { .await } +pub async fn create_http_client_async_with_url_strict(url: &str) -> ResultType { + let parsed_url = url::Url::parse(url)?; + if parsed_url.scheme() != "https" { + bail!("Strict HTTP client requires HTTPS: {}", url); + } + let proxy_conf = Config::get_socks(); + let tls_url = get_url_for_tls(url, &proxy_conf); + let cached_tls_type = get_cached_tls_type(tls_url); + let cached_danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url); + let can_reuse_cached_probe = + cached_tls_type.is_some() && cached_danger_accept_invalid_cert == Some(false); + let tls_type = if can_reuse_cached_probe { + cached_tls_type.unwrap_or(TlsType::Rustls) + } else { + TlsType::Rustls + }; + Ok(create_http_client_async_with_url_( + url, + tls_url, + tls_type, + can_reuse_cached_probe, + Some(false), + Some(false), + ) + .await) +} + #[async_recursion] async fn create_http_client_async_with_url_( url: &str, diff --git a/src/updater.rs b/src/updater.rs index 56fdc1d2a8c..bf923dd56ed 100644 --- a/src/updater.rs +++ b/src/updater.rs @@ -1,8 +1,8 @@ -use crate::{common::do_check_software_update, hbbs_http::create_http_client_with_url}; +use crate::{common::do_check_software_update, hbbs_http::create_http_client_with_url_strict}; use hbb_common::{bail, config, log, ResultType}; use std::{ io::Write, - path::PathBuf, + path::{Component, Path, PathBuf}, sync::{ atomic::{AtomicUsize, Ordering}, mpsc::{channel, Receiver, Sender}, @@ -153,7 +153,7 @@ fn check_update(manually: bool) -> ResultType<()> { format!("{}/rustdesk-{}-x86-sciter.exe", download_url, version) }; log::debug!("New version available: {}", &version); - let client = create_http_client_with_url(&download_url); + let client = create_http_client_with_url_strict(&download_url)?; let Some(file_path) = get_download_file_from_url(&download_url) else { bail!("Failed to get the file path from the URL: {}", download_url); }; @@ -291,7 +291,96 @@ fn update_new_version(update_msi: bool, version: &str, file_path: &PathBuf) { } } -pub fn get_download_file_from_url(url: &str) -> Option { - let filename = url.split('/').last()?; +pub fn get_update_download_file_from_url(url: &str) -> Option { + let parsed = url::Url::parse(url).ok()?; + // Check the raw prefix before Url normalizes default ports. + if !url.starts_with("https://github.com/") + || parsed.scheme() != "https" + || parsed.host_str() != Some("github.com") + || !parsed.username().is_empty() + || parsed.password().is_some() + || parsed.port().is_some() + || parsed.query().is_some() + || parsed.fragment().is_some() + { + return None; + } + + let mut segments = parsed.path_segments()?; + let owner = segments.next()?; + let repo = segments.next()?; + let releases = segments.next()?; + let download = segments.next()?; + let tag = segments.next()?; + let filename = segments.next()?; + + if owner != "rustdesk" + || repo != "rustdesk" + || releases != "releases" + || download != "download" + || tag.is_empty() + || segments.next().is_some() + || !is_plain_update_filename(filename) + { + return None; + } + Some(std::env::temp_dir().join(filename)) } + +fn is_plain_update_filename(filename: &str) -> bool { + if filename.is_empty() + || filename.contains('/') + || filename.contains('\\') + || filename.contains(':') + { + return false; + } + + let mut components = Path::new(filename).components(); + matches!( + components.next(), + Some(Component::Normal(name)) if name.to_str() == Some(filename) + ) && components.next().is_none() +} + +pub fn get_download_file_from_url(url: &str) -> Option { + get_update_download_file_from_url(url) +} + +#[cfg(test)] +mod tests { + use super::get_download_file_from_url; + + #[test] + fn update_download_file_accepts_expected_github_asset_urls() { + let file = get_download_file_from_url( + "https://github.com/rustdesk/rustdesk/releases/download/1.4.0/rustdesk-1.4.0-x86_64.dmg", + ) + .expect("valid GitHub release asset URL"); + + assert_eq!( + file.file_name().and_then(|name| name.to_str()), + Some("rustdesk-1.4.0-x86_64.dmg") + ); + } + + #[test] + fn update_download_file_rejects_untrusted_or_malformed_urls() { + for url in [ + "http://github.com/rustdesk/rustdesk/releases/download/1/rustdesk.exe", + "https://example.com/rustdesk.exe", + "https://github.com/other/project/releases/download/1/rustdesk.exe", + "https://github.com/rustdesk/rustdesk/releases/download/1/", + "https://github.com/rustdesk/rustdesk/releases/download/1/nested/rustdesk.exe", + "https://github.com/rustdesk/rustdesk/releases/download/1/C:rustdesk.exe", + "https://user@github.com/rustdesk/rustdesk/releases/download/1/rustdesk.exe", + "https://github.com:443/rustdesk/rustdesk/releases/download/1/rustdesk.exe", + "https://github.com/rustdesk/rustdesk/releases/download/1/rustdesk.exe?download=1", + "https://github.com/rustdesk/rustdesk/releases/download/1/rustdesk.exe#download", + "not a url", + ] { + assert!(get_download_file_from_url(url).is_none(), "{url}"); + } + } +}