diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 3a80028..3b3ad41 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -17,8 +17,8 @@ argon2 = { features = ["alloc", "password-hash"], default-features = false, vers async_zip = { features = ["deflate", "tokio"], default-features = false, version = "0.0.17" } async-stream = "0.3.6" async-walkdir = "2.0.0" -axum = { version = "0.7.7", features = ["http2", "json", "query", "tokio"], default-features = false } -axum-extra = { version = "0.9.4", features = ["cookie-private", "typed-header"], default-features = false } +axum = { version = "0.7.9", features = ["http2", "json", "query", "tokio"], default-features = false } +axum-extra = { version = "0.9.6", features = ["cookie-private", "typed-header"], default-features = false } axum-server = "0.7.1" base64ct = { version = "1.6.0", features = ["alloc"] } chacha20poly1305 = { version = "0.10.1", features = ["stream"], default-features = false } @@ -29,7 +29,7 @@ futures-util = { default-features = false, version = "0.3.31" } headers = "0.4.0" http = "1.1.0" http-body-util = "0.1.2" -hyper = { version = "1.5.0", default-features = false } +hyper = { version = "1.5.1", default-features = false } hyper-util = { version = "0.1.10", features = ["client-legacy", "http1", "tokio"], default-features = false } hyper-rustls = { version = "0.27.3", features = ["http1", "http2", "ring", "tls12", "webpki-tokio"], default-features = false } hyper-hickory = { version = "0.7.0", default-features = false, features = ["system-config"] } @@ -39,13 +39,14 @@ mime_guess = { default-features = false, version = "2.0.5" } # TEMPORARY oauth2 = { version = "5.0.0-rc.1", default-features = false } percent-encoding = { default-features = false, version = "2.3.1" } -quick-xml = "0.37.0" +quick-xml = "0.37.1" rand = { default-features = false, version = "0.8.5" } -rustls = { default-features = false, version = "0.23.16", features = ["ring"] } +rcgen = { version = "0.13.1", default-features = false, optional = true } +rustls = { default-features = false, version = "0.23.18", features = ["ring"] } rustls-pki-types = { version = "1.10.0" } rustls-acme = { version = "0.12.1", features = ["axum", "ring"], default-features = false } serde = { version = "1.0.215", default-features = false } -serde_json = { default-features = false, version = "1.0.132" } +serde_json = { default-features = false, version = "1.0.133" } serde_yml = "0.0.12" sha2 = { default-features = false, version = "0.10.8" } sysinfo = { default-features = false, version = "0.32.0", features = ["disk", "system"] } @@ -54,7 +55,7 @@ tokio = { version = "1.41.1", features = ["full"], default-features = false } tokio-stream = { version = "0.1.16", default-features = false } tokio-util = { version = "0.7.12", default-features = false } tower = { default-features = false, version = "0.5.1", features = ["util"] } -tower-http = { version = "0.6.1", features = ["fs"], default-features = false } +tower-http = { version = "0.6.2", features = ["fs"], default-features = false } tower-service = "0.3.3" tracing = { default-features = false, version = "0.1.40" } tracing-appender = "0.2.3" @@ -63,6 +64,10 @@ trim-in-place = "0.1.7" urlencoding = "2.1.3" uuid = { version = "1.11.0", features = ["fast-rng", "v4"], default-features = false } +[features] +default = ["self_signed"] +self_signed = ["dep:rcgen"] + [dev-dependencies] async-tungstenite = { version = "0.28.0", features = ["tokio-runtime"] } reqwest = { version = "0.12.9", default-features = false, features = ["cookies", "json", "rustls-tls", "stream"] } diff --git a/backend/atrium.yaml b/backend/atrium.yaml index 8c7cec6..34aea26 100644 --- a/backend/atrium.yaml +++ b/backend/atrium.yaml @@ -3,7 +3,7 @@ hostname: atrium.127.0.0.1.nip.io # required : fully qualified domain name of th debug_mode: true # optional, defaults to false : prints a lot of debug logs ; disable in production as it has a big performance impact single_proxy: false # optional, default to false : in single proxy mode, atrium will route only to the first app available, it is meant to secure a single proxied application with Open ID Connect http_port: 8080 # required, defaults to 8080 : http port to listen to if tls mode is not Auto -tls_mode: No # required, defaults to No : use No for development/test http mode, Auto to generate Let's Encrypt certificates automatically (most common production usage) or ̀BehindProxy to use atrium behind a TLS offloading proxy +tls_mode: No # required, defaults to No : use No for development/test http mode, Auto to generate Let's Encrypt certificates automatically (most common production usage) or ̀BehindProxy to use atrium behind a TLS offloading proxy or SelfSigned to generate self signed certificates (using http_port for https) letsencrypt_email: foo@bar.com # required if `tls_mode: Auto` is used : email for receiving Let's Encrypt information #cookie_key : # required, will be generated on first start : cookies and token signing key !!! SENSITIVE INFORMATION : TO BE KEPT HIDDEN !!! log_to_file: false # optional, defaults to false : log to a file in addition to std out diff --git a/backend/src/configuration.rs b/backend/src/configuration.rs index 5a9ed3f..8e9c89f 100644 --- a/backend/src/configuration.rs +++ b/backend/src/configuration.rs @@ -59,6 +59,8 @@ pub enum TlsMode { No, BehindProxy, Auto, + #[cfg(feature = "self_signed")] + SelfSigned, } impl TlsMode { @@ -66,6 +68,8 @@ impl TlsMode { match self { TlsMode::No => false, TlsMode::BehindProxy | TlsMode::Auto => true, + #[cfg(feature = "self_signed")] + TlsMode::SelfSigned => true, } } } @@ -331,7 +335,7 @@ impl HostType { pub fn secured(&self) -> bool { match self { - HostType::ReverseApp(app)| HostType::SkipVerifyReverseApp(app)=> app.inner.secured, + HostType::ReverseApp(app) | HostType::SkipVerifyReverseApp(app) => app.inner.secured, HostType::Dav(dav) => dav.secured, HostType::StaticApp(app) => app.secured, } diff --git a/backend/src/main.rs b/backend/src/main.rs index 57bcbfc..e9a2604 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -24,12 +24,17 @@ use tracing::{error, info}; use tracing_appender::non_blocking::WorkerGuard; use tracing_subscriber::{fmt, fmt::time::OffsetTime, prelude::*}; +#[cfg(feature = "self_signed")] +pub mod self_signed; + pub const CONFIG_FILE: &str = "atrium.yaml"; fn main() -> Result<()> { // println!("MiMalloc version: {}", mimalloc::MiMalloc.version()); // mimalloc = { version = "0.1", features = ["extended"] } in Cargo.toml to use this // We need to work out the local time offset before entering multi-threaded context - let cfg: Config = if let Ok(file) = File::open(CONFIG_FILE) { serde_yml::from_reader(file).expect("failed to parse configuration file") } else { + let cfg: Config = if let Ok(file) = File::open(CONFIG_FILE) { + serde_yml::from_reader(file).expect("failed to parse configuration file") + } else { println!("Configuration file not found, trying to create default configuration file."); File::create(CONFIG_FILE).expect("could not create default configuration file"); Config::default() @@ -97,52 +102,61 @@ async fn run() -> Result<()> { } }); - if config.0.tls_mode == TlsMode::Auto { - let config = atrium::configuration::load_config(CONFIG_FILE).await?; - let domains: Vec = config.0.domains(); - info!( - "Getting let's encrypt certificates for FQDNs : {:?}", - domains - ); - let mut state = AcmeConfig::new(domains) - .contact_push(format!("mailto:{}", config.0.letsencrypt_email)) - .directory_lets_encrypt(true) - .cache(DirCache::new("./letsencrypt_cache")) - .state(); - - let mut rustls_config = ServerConfig::builder() - .with_no_client_auth() - .with_cert_resolver(state.resolver()); - rustls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; - let acceptor = state.axum_acceptor(Arc::new(rustls_config)); - - tokio::spawn(async move { - loop { - match state.next().await.unwrap() { - Ok(ok) => info!("ACME (let's encrypt) event: {:?}", ok), - Err(err) => error!("ACME (let's encrypt) error: {:?}", err), + match config.0.tls_mode { + TlsMode::Auto => { + let config = atrium::configuration::load_config(CONFIG_FILE).await?; + let domains: Vec = config.0.domains(); + info!( + "Getting let's encrypt certificates for FQDNs : {:?}", + domains + ); + let mut state = AcmeConfig::new(domains) + .contact_push(format!("mailto:{}", config.0.letsencrypt_email)) + .directory_lets_encrypt(true) + .cache(DirCache::new("./letsencrypt_cache")) + .state(); + + let mut rustls_config = ServerConfig::builder() + .with_no_client_auth() + .with_cert_resolver(state.resolver()); + rustls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; + let acceptor = state.axum_acceptor(Arc::new(rustls_config)); + + tokio::spawn(async move { + loop { + match state.next().await.expect("could not start ACME loop") { + Ok(ok) => info!("ACME (let's encrypt) event: {:?}", ok), + Err(err) => error!("ACME (let's encrypt) error: {:?}", err), + } } - } - }); + }); - // Spawn a server to redirect HTTP to HTTPS - tokio::spawn(redirect_http_to_https(handle.clone())); + // Spawn a server to redirect HTTP to HTTPS + tokio::spawn(redirect_http_to_https(handle.clone())); - // Main server - let addr = format!("[::]:{}", 443) - .parse::() - .unwrap(); + // Main server + let addr = format!("[::]:{}", 443).parse::()?; - axum_server::bind(addr) - .acceptor(acceptor) - .handle(handle) - .serve(app) + axum_server::bind(addr) + .acceptor(acceptor) + .handle(handle) + .serve(app) + .await?; + } + #[cfg(feature = "self_signed")] + TlsMode::SelfSigned => { + self_signed::serve_with_self_signed_cert( + ip_bind, + &server.port, + handle, + app, + ) .await?; - } else { - let addr = format!("{ip_bind}:{}", server.port) - .parse::() - .unwrap(); - axum_server::bind(addr).handle(handle).serve(app).await?; + } + _ => { + let addr = format!("{ip_bind}:{}", server.port).parse::()?; + axum_server::bind(addr).handle(handle).serve(app).await?; + } } } @@ -229,7 +243,7 @@ async fn redirect_http_to_https(handle: Handle) -> tokio::io::Result<()> { let mut parts = uri.into_parts(); parts.scheme = Some(axum::http::uri::Scheme::HTTPS); if parts.path_and_query.is_none() { - parts.path_and_query = Some("/".parse().unwrap()); + parts.path_and_query = Some("/".parse()?); } parts.authority = Some(host.parse()?); Ok(Uri::from_parts(parts)?) diff --git a/backend/src/self_signed.rs b/backend/src/self_signed.rs new file mode 100644 index 0000000..1cecc56 --- /dev/null +++ b/backend/src/self_signed.rs @@ -0,0 +1,65 @@ +use crate::CONFIG_FILE; +use anyhow::Result; +use axum::{extract::connect_info::IntoMakeServiceWithConnectInfo, routing::MethodRouter}; +use axum_server::{tls_rustls::RustlsConfig, Handle}; +use std::{net::SocketAddr, path::Path}; +use tokio::fs; +use tracing::info; + +const CERT_PATH: &str = "cert.pem"; +const KEY_PATH: &str = "key.pem"; + +pub async fn serve_with_self_signed_cert( + ip: &str, + port: &u16, + handle: Handle, + app: IntoMakeServiceWithConnectInfo, +) -> anyhow::Result<()> { + // Certificates + let (cert, key) = load_or_generate_cert().await?; + let rustls_config = RustlsConfig::from_pem(cert, key).await?; + + // Main server + let addr = format!("{ip}:{}", port).parse::()?; + + // Start the server with TLS + Ok(axum_server::bind_rustls(addr, rustls_config) + .handle(handle) + .serve(app) + .await?) +} + +/// Load or generate a self-signed certificate and private key +async fn load_or_generate_cert() -> Result<(Vec, Vec)> { + if Path::new(CERT_PATH).exists() && Path::new(KEY_PATH).exists() { + info!("Loading existing certificate and key from disk..."); + let cert = fs::read(CERT_PATH).await?; + let key = fs::read(KEY_PATH).await?; + Ok((cert, key)) + } else { + info!("Generating new self-signed certificate and key..."); + let (cert, key) = generate_self_signed_cert().await?; + persist_cert_and_key(&cert, &key).await?; + Ok((cert, key)) + } +} + +/// Generate a self-signed certificate and private key +async fn generate_self_signed_cert() -> Result<(Vec, Vec)> { + let config = atrium::configuration::load_config(CONFIG_FILE).await?; + let domains: Vec = config.0.domains(); + // Generate a self-signed certificate using rcgen + let cert = rcgen::generate_simple_self_signed(domains)?; + Ok(( + cert.cert.pem().into_bytes(), + cert.key_pair.serialize_pem().into_bytes(), + )) +} + +/// Persist the certificate and key to files +async fn persist_cert_and_key(cert: &[u8], key: &[u8]) -> Result<()> { + info!("Persisting certificate and key to disk..."); + fs::write(CERT_PATH, cert).await?; + fs::write(KEY_PATH, key).await?; + Ok(()) +} diff --git a/frontend/lib/components/explorer.dart b/frontend/lib/components/explorer.dart index ac221be..eeaf235 100644 --- a/frontend/lib/components/explorer.dart +++ b/frontend/lib/components/explorer.dart @@ -223,19 +223,13 @@ class ExplorerState extends State { } Widget _buildListView(BuildContext context, List list) { - if (list.isNotEmpty && list[0].path == null) { - list.removeAt(0); - } // Remove the ".." placeholder it it has been inserted before rebuild - if (sortBy == SortBy.names) { list.sort(foldersFirstThenAlphabetically); } else { list.sort((a, b) => b.mTime!.compareTo(a.mTime!)); } - if (dirPath != "/") list.insert(0, File()); - - for (var i = 1; i < list.length; i++) { + for (var i = 0; i < list.length; i++) { // If we found a file before building this view, we scroll to that file if (list[i].path! == foundFile) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -243,16 +237,16 @@ class ExplorerState extends State { index: i, ); }); + break; } } CancelToken cancelToken = CancelToken(); - return ScrollablePositionedList.builder( - itemCount: list.length, - itemBuilder: (context, index) { - if (dirPath != "/" && index == 0) { - return ListTile( + return Column( + children: [ + if (dirPath != "/") + ListTile( leading: const Icon(Icons.reply), title: const Text(".."), onTap: () { @@ -262,249 +256,261 @@ class ExplorerState extends State { _getData(); }); }, - ); - } else { - var file = list[index]; - var type = fileType(file); + ), + Expanded( + child: ScrollablePositionedList.builder( + itemCount: list.length, + itemBuilder: (context, index) { + var file = list[index]; + var type = fileType(file); - return ListTile( - leading: widgetFromFileType(file, type, cancelToken), - tileColor: file.path! == foundFile ? Colors.grey[400] : null, - title: Text(file.name ?? ''), - subtitle: Text(formatTime(file.mTime) + - ((file.size != null && file.size! > 0) - ? " - ${filesize(file.size, 0)}" - : "")), - trailing: PopupMenuButton( - itemBuilder: (BuildContext context) => [ - PopupMenuItem( - onTap: () => - download(widget.url, client, file, context), - child: Row( - children: [ - const Padding( - padding: EdgeInsets.all(8.0), - child: Icon(Icons.download), - ), - Text(tr(context, "download")) - ], - )), - PopupMenuItem( - onTap: () { - WidgetsBinding.instance - .addPostFrameCallback((_) async { - if (!widget.dav.secured) { - Clipboard.setData(ClipboardData( - text: - '${widget.url}${escapePath(file.path!)}')); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - tr(context, "share_url_copied")))); - } else { - await showDialog( - context: context, - builder: (context) => - ShareDialog(widget.url, file, client)); - } - }); - }, - child: Row( - children: [ - const Padding( - padding: EdgeInsets.all(8.0), - child: Icon(Icons.share), - ), - Text(tr(context, "share")) - ], - )), - if (readWrite) ...[ - PopupMenuItem( - onTap: () { - WidgetsBinding.instance - .addPostFrameCallback((_) async { - String? val = await showDialog( - context: context, - builder: (context) => - RenameDialog(file.name!), - ); - if (val != null && file.path != null) { - var newPath = file.path!; - newPath = newPath.endsWith("/") - ? newPath.substring(0, newPath.length - 1) - : newPath; - newPath = - "${newPath.substring(0, newPath.lastIndexOf('/'))}/$val"; - newPath = file.isDir! ? "$newPath/" : newPath; - await client.rename( - file.path!, newPath, true); + return ListTile( + leading: widgetFromFileType(file, type, cancelToken), + tileColor: file.path! == foundFile ? Colors.grey[400] : null, + title: Text(file.name ?? ''), + subtitle: Text(formatTime(file.mTime) + + ((file.size != null && file.size! > 0) + ? " - ${filesize(file.size, 0)}" + : "")), + trailing: PopupMenuButton( + itemBuilder: (BuildContext context) => [ + PopupMenuItem( + onTap: () => + download(widget.url, client, file, context), + child: Row( + children: [ + const Padding( + padding: EdgeInsets.all(8.0), + child: Icon(Icons.download), + ), + Text(tr(context, "download")) + ], + )), + PopupMenuItem( + onTap: () { + WidgetsBinding.instance + .addPostFrameCallback((_) async { + if (!widget.dav.secured) { + Clipboard.setData(ClipboardData( + text: + '${widget.url}${escapePath(file.path!)}')); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(tr( + context, "share_url_copied")))); + } else { + await showDialog( + context: context, + builder: (context) => ShareDialog( + widget.url, file, client)); + } + }); + }, + child: Row( + children: [ + const Padding( + padding: EdgeInsets.all(8.0), + child: Icon(Icons.share), + ), + Text(tr(context, "share")) + ], + )), + if (readWrite) ...[ + PopupMenuItem( + onTap: () { + WidgetsBinding.instance + .addPostFrameCallback((_) async { + String? val = await showDialog( + context: context, + builder: (context) => + RenameDialog(file.name!), + ); + if (val != null && file.path != null) { + var newPath = file.path!; + newPath = newPath.endsWith("/") + ? newPath.substring( + 0, newPath.length - 1) + : newPath; + newPath = + "${newPath.substring(0, newPath.lastIndexOf('/'))}/$val"; + newPath = + file.isDir! ? "$newPath/" : newPath; + await client.rename( + file.path!, newPath, true); + setState(() { + _getData(); + }); + } + }); + }, + child: Row( + children: [ + const Padding( + padding: EdgeInsets.all(8.0), + child: + Icon(Icons.drive_file_rename_outline), + ), + Text(tr(context, "rename")) + ], + )), + PopupMenuItem( + onTap: (() { setState(() { - _getData(); + _copyMoveStatus = CopyMoveStatus.copy; + _copyMovePath = file.path!; }); - } - }); - }, - child: Row( - children: [ - const Padding( - padding: EdgeInsets.all(8.0), - child: Icon(Icons.drive_file_rename_outline), - ), - Text(tr(context, "rename")) - ], - )), - PopupMenuItem( - onTap: (() { - setState(() { - _copyMoveStatus = CopyMoveStatus.copy; - _copyMovePath = file.path!; - }); - }), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Icon(Icons.copy, - color: _copyMovePath == file.path! && - _copyMoveStatus == - CopyMoveStatus.copy - ? Colors.blueAccent - : null), - ), - Text(tr(context, "copy")) - ], - )), - PopupMenuItem( - onTap: (() { - setState(() { - _copyMoveStatus = CopyMoveStatus.move; - _copyMovePath = file.path!; - }); - }), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Icon(Icons.cut, - color: _copyMovePath == file.path! && - _copyMoveStatus == - CopyMoveStatus.move - ? Colors.blueAccent - : null), - ), - Text(tr(context, "cut")) - ], - )), - PopupMenuItem( - onTap: () async { - WidgetsBinding.instance - .addPostFrameCallback((_) async { - var confirmed = await showDialog( - context: context, - builder: (context) => - DeleteDialog(file.name!), - ); - if (confirmed!) { - await client.removeAll(file.path!); + }), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Icon(Icons.copy, + color: _copyMovePath == file.path! && + _copyMoveStatus == + CopyMoveStatus.copy + ? Colors.blueAccent + : null), + ), + Text(tr(context, "copy")) + ], + )), + PopupMenuItem( + onTap: (() { setState(() { - list.removeAt(index); + _copyMoveStatus = CopyMoveStatus.move; + _copyMovePath = file.path!; }); - } - }); - }, - child: Row( - children: [ - const Padding( - padding: EdgeInsets.all(8.0), - child: Icon(Icons.delete), - ), - Text(tr(context, "delete")) - ], - )) - ] - ]), - onTap: () async { - if (file.isDir!) { - dirPath = file.path!; - setState(() { - _getData(); - }); - } else { - if (type == FileType.text) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => TextEditor( - client: client, file: file, readWrite: readWrite)), - ); - } else if (type == FileType.image) { - cancelToken.cancel(); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ImageViewer( - client: client, - url: widget.url, - files: list, - index: index, - )), - ); - } else if (type == FileType.pdf) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PdfViewer( - client: client, - url: widget.url, - file: file, - color: widget.dav.color)), - ); - } else if (type == FileType.document) { - // Get a share token for this document - var shareToken = await ApiProvider().getShareToken( - widget.url.split("://")[1].split(":")[0], file.path!, - shareWith: "external_editor", shareForDays: 1); - final Uri launchUri = Uri( - scheme: App().prefs.hostnameScheme, - host: App().prefs.hostnameHost, - port: App().prefs.hostnamePort, - path: 'onlyoffice', - query: joinQueryParameters({ - 'file': '${widget.url}${file.path}', - 'mtime': file.mTime!.toIso8601String(), - 'user': App().prefs.username, - 'share_token': shareToken! - }), - ); - if (!context.mounted) return; - Navigator.of(context) - .push(MaterialPageRoute(builder: (context) { - return Scaffold( - appBar: AppBar(toolbarHeight: 0.0), - body: AppWebView(initialUrl: launchUri.toString())); - })); - } else if (type == FileType.media) { - String uri = '${widget.url}${escapePath(file.path!)}'; - if (kIsWeb) { - var shareToken = await ApiProvider().getShareToken( - widget.url.split("://")[1].split(":")[0], file.path!, - shareWith: "media_player", shareForDays: 1); - uri = '$uri?token=$shareToken'; + }), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Icon(Icons.cut, + color: _copyMovePath == file.path! && + _copyMoveStatus == + CopyMoveStatus.move + ? Colors.blueAccent + : null), + ), + Text(tr(context, "cut")) + ], + )), + PopupMenuItem( + onTap: () async { + WidgetsBinding.instance + .addPostFrameCallback((_) async { + var confirmed = await showDialog( + context: context, + builder: (context) => + DeleteDialog(file.name!), + ); + if (confirmed!) { + await client.removeAll(file.path!); + setState(() { + list.removeAt(index); + }); + } + }); + }, + child: Row( + children: [ + const Padding( + padding: EdgeInsets.all(8.0), + child: Icon(Icons.delete), + ), + Text(tr(context, "delete")) + ], + )) + ] + ]), + onTap: () async { + if (file.isDir!) { + dirPath = file.path!; + setState(() { + _getData(); + }); + } else { + if (type == FileType.text) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TextEditor( + client: client, + file: file, + readWrite: readWrite)), + ); + } else if (type == FileType.image) { + cancelToken.cancel(); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ImageViewer( + client: client, + url: widget.url, + files: list, + index: index, + )), + ); + } else if (type == FileType.pdf) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PdfViewer( + client: client, + url: widget.url, + file: file, + color: widget.dav.color)), + ); + } else if (type == FileType.document) { + // Get a share token for this document + var shareToken = await ApiProvider().getShareToken( + widget.url.split("://")[1].split(":")[0], file.path!, + shareWith: "external_editor", shareForDays: 1); + final Uri launchUri = Uri( + scheme: App().prefs.hostnameScheme, + host: App().prefs.hostnameHost, + port: App().prefs.hostnamePort, + path: 'onlyoffice', + query: joinQueryParameters({ + 'file': '${widget.url}${file.path}', + 'mtime': file.mTime!.toIso8601String(), + 'user': App().prefs.username, + 'share_token': shareToken! + }), + ); + if (!context.mounted) return; + Navigator.of(context) + .push(MaterialPageRoute(builder: (context) { + return Scaffold( + appBar: AppBar(toolbarHeight: 0.0), + body: AppWebView(initialUrl: launchUri.toString())); + })); + } else if (type == FileType.media) { + String uri = '${widget.url}${escapePath(file.path!)}'; + if (kIsWeb) { + var shareToken = await ApiProvider().getShareToken( + widget.url.split("://")[1].split(":")[0], + file.path!, + shareWith: "media_player", + shareForDays: 1); + uri = '$uri?token=$shareToken'; + } + if (!context.mounted) return; + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + MediaPlayer(uri: uri, file: file))); + } } - if (!context.mounted) return; - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - MediaPlayer(uri: uri, file: file))); - } - } + }, + ); }, - ); - } - }, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ), + ), + ], ); } diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index c726ef1..c8e71f8 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -385,10 +385,10 @@ packages: dependency: "direct main" description: name: open_file - sha256: "737641e823d568a12b63494855010ceef286bcdf8f88d0a831e53229a5e850e8" + sha256: d17e2bddf5b278cb2ae18393d0496aa4f162142ba97d1a9e0c30d476adf99c0e url: "https://pub.dev" source: hosted - version: "3.5.9" + version: "3.5.10" open_file_android: dependency: transitive description: @@ -417,10 +417,10 @@ packages: dependency: transitive description: name: open_file_mac - sha256: dd1570bd12601b4d50fda3609c1662382f17ee403b47f0d74d737de603a39ec6 + sha256: "1440b1e37ceb0642208cfeb2c659c6cda27b25187a90635c9d1acb7d0584d324" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.3" open_file_platform_interface: dependency: transitive description: @@ -871,10 +871,10 @@ packages: dependency: "direct main" description: name: webview_flutter_android - sha256: "74693a212d990b32e0b7055d27db973a18abf31c53942063948cdfaaef9787ba" + sha256: "285cedfd9441267f6cca8843458620b5fda1af75b04f5818d0441acda5d7df19" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.1.0" webview_flutter_platform_interface: dependency: transitive description: diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index cb3d8de..1a87900 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -46,13 +46,13 @@ dependencies: video_player: ^2.9.2 webdav_client: ^1.2.2 webview_flutter: ^4.10.0 - webview_flutter_android: ^4.0.0 + webview_flutter_android: ^4.1.0 # TEMPORARY webview_cookie_manager: git: https://github.com/fryette/webview_cookie_manager.git path: ^1.9.0 scrollable_positioned_list: ^0.3.8 - open_file: ^3.5.9 + open_file: ^3.5.10 dev_dependencies: flutter_test: