Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/v0.1.0-beta4.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ SYNC
- Linked devices check for devices asking to join every 5 seconds only while the Sync page is on screen, and the check no longer reads the whole device list from the server.
- A new device that starts from a snapshot whose data can't be read now starts from an older snapshot instead of failing the sync.
- Linked devices: allowing or declining remote control now takes effect whichever profile is open.
- Linked devices reach a Flow server or WebDAV folder on your local network directly, even when a system proxy is set.
- Linked devices sign in to your Flow server about once every 15 minutes instead of before every sync.

MUSIC
Expand Down
20 changes: 18 additions & 2 deletions src-tauri/src/sync2/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,19 @@ pub async fn read_body(mut response: Response, limit: usize) -> Result<Vec<u8>>
pub struct Transport {
/// A `.local` name over plain HTTP, checked and pinned so DNS can't move it afterwards.
pinned: Option<(String, SocketAddr)>,
/// A proxy would move a plain request, credentials included, off the checked address.
direct: bool,
}

impl Transport {
pub async fn for_url(url: &Url) -> Result<Self> {
match url.scheme() {
"https" => return Ok(Self { pinned: None }),
"https" => {
return Ok(Self {
pinned: None,
direct: false,
});
}
"http" => {}
_ => return Err(StoreError::Insecure),
}
Expand All @@ -42,7 +49,10 @@ impl Transport {
.ok_or(StoreError::Insecure)?
.trim_matches(['[', ']']);
if host.parse::<IpAddr>().is_ok_and(is_local_ip) {
return Ok(Self { pinned: None });
return Ok(Self {
pinned: None,
direct: true,
});
}
if !host.to_ascii_lowercase().ends_with(".local") {
return Err(StoreError::Insecure);
Expand All @@ -55,6 +65,7 @@ impl Transport {
match addresses.first() {
Some(first) if addresses.iter().all(|a| is_local_ip(a.ip())) => Ok(Self {
pinned: Some((host.to_owned(), *first)),
direct: true,
}),
_ => Err(StoreError::Insecure),
}
Expand All @@ -66,6 +77,11 @@ impl Transport {
.redirect(redirect::Policy::none())
.user_agent(concat!("FlowDesktop/", env!("CARGO_PKG_VERSION")))
.connect_timeout(Duration::from_secs(10));
let builder = if self.direct {
builder.no_proxy()
} else {
builder
};
match &self.pinned {
Some((host, address)) => builder.resolve(host, *address),
None => builder,
Expand Down
57 changes: 57 additions & 0 deletions src-tauri/tests/sync2_http_proxy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//! Plain HTTP goes only to the private address that was checked (spec/store.md, Transport
//! security). This test runs alone in its process because it sets the proxy environment reqwest
//! reads.

use std::time::Duration;

use flow_desktop_lib::sync2::webdav::{WebDav, WebDavLogin};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;

/// Answers one request with 404 and returns the first line it read.
async fn answer_one(listener: TcpListener) -> String {
let (mut stream, _) = listener.accept().await.unwrap();
let mut request = vec![0; 4096];
let read = stream.read(&mut request).await.unwrap();
let _ = stream
.write_all(b"HTTP/1.1 404 Not Found\r\ncontent-length: 0\r\nconnection: close\r\n\r\n")
.await;
let text = String::from_utf8_lossy(&request[..read]);
text.lines().next().unwrap_or_default().to_owned()
}

#[tokio::test]
async fn plain_http_to_a_private_address_skips_a_system_proxy() {
let proxy = TcpListener::bind("127.0.0.1:0").await.unwrap();
let store = TcpListener::bind("127.0.0.1:0").await.unwrap();
let store_address = store.local_addr().unwrap();
// Before any client is built; nothing else in this process reads the environment.
unsafe {
let address = proxy.local_addr().unwrap();
std::env::set_var("HTTP_PROXY", format!("http://{address}"));
std::env::set_var("http_proxy", format!("http://{address}"));
std::env::remove_var("NO_PROXY");
std::env::remove_var("no_proxy");
}
let through_proxy = tokio::spawn(answer_one(proxy));
let reached_store = tokio::spawn(answer_one(store));

let login = WebDavLogin {
user: "flow".into(),
password: "secret".into(),
};
let client = WebDav::connect(&format!("http://{store_address}/flow-sync/"), login)
.await
.unwrap();
client.get("x").await.unwrap();

assert!(
!through_proxy.is_finished(),
"the request went through the proxy"
);
let line = tokio::time::timeout(Duration::from_secs(5), reached_store).await;
assert_eq!(
line.expect("the store didn't get the request").unwrap(),
"GET /flow-sync/x HTTP/1.1"
);
}
Loading