From c1171e83763135efbe3865b5a8410444ae3574b6 Mon Sep 17 00:00:00 2001 From: Armin Friedl Date: Sun, 31 May 2020 14:41:04 +0200 Subject: [PATCH] Direct download Implement direct download with online packaging --- .../fling/controller/FlingController.java | 10 +++- .../fling/security/AuthorizationService.java | 8 ++- .../security/FlingWebSecurityConfigurer.java | 15 +++-- .../friedl/fling/service/FlingService.java | 15 ++++- .../src/components/user/DirectDownload.jsx | 55 +++++++++++++++++++ web/fling/src/components/user/FlingUser.jsx | 4 +- web/fling/src/style/fling.scss | 10 ++++ 7 files changed, 104 insertions(+), 13 deletions(-) create mode 100644 web/fling/src/components/user/DirectDownload.jsx diff --git a/service/fling/src/main/java/net/friedl/fling/controller/FlingController.java b/service/fling/src/main/java/net/friedl/fling/controller/FlingController.java index 1e85ce9..cf5374f 100644 --- a/service/fling/src/main/java/net/friedl/fling/controller/FlingController.java +++ b/service/fling/src/main/java/net/friedl/fling/controller/FlingController.java @@ -69,11 +69,15 @@ public class FlingController { flingService.deleteFlingById(flingId); } + @GetMapping(path = "/fling/{flingId}/package") + public String packageFling(@PathVariable Long flingId) throws IOException, ArchiveException { + return flingService.packageFling(flingId); + } - @GetMapping(path = "/fling/{flingId}/download") - public ResponseEntity downloadFling(@PathVariable Long flingId) throws ArchiveException, IOException { + @GetMapping(path = "/fling/{flingId}/download/{downloadId}") + public ResponseEntity downloadFling(@PathVariable Long flingId, @PathVariable String downloadId) throws ArchiveException, IOException { var fling = flingService.findFlingById(flingId).orElseThrow(); - var flingPackage = flingService.packageFling(flingId); + var flingPackage = flingService.downloadFling(downloadId); var stream = new InputStreamResource(flingPackage.getFirst()); return ResponseEntity.ok() diff --git a/service/fling/src/main/java/net/friedl/fling/security/AuthorizationService.java b/service/fling/src/main/java/net/friedl/fling/security/AuthorizationService.java index 1cd9e45..ab98525 100644 --- a/service/fling/src/main/java/net/friedl/fling/security/AuthorizationService.java +++ b/service/fling/src/main/java/net/friedl/fling/security/AuthorizationService.java @@ -29,8 +29,12 @@ public class AuthorizationService { return userAuth.getShareId().equals(shareUrl); } - public boolean allowFlingAccess(Long flingId) { - return false; + public boolean allowFlingAccess(Long flingId, FlingToken authentication) { + if(authentication.getGrantedFlingAuthority().getAuthority().equals(FlingAuthority.FLING_OWNER.name())) { + return true; + } + + return authentication.getGrantedFlingAuthority().getFlingId().equals(flingId); } public boolean allowFlingAccess(FlingToken authentication, HttpServletRequest request) { diff --git a/service/fling/src/main/java/net/friedl/fling/security/FlingWebSecurityConfigurer.java b/service/fling/src/main/java/net/friedl/fling/security/FlingWebSecurityConfigurer.java index 7f80c47..9a5b5df 100644 --- a/service/fling/src/main/java/net/friedl/fling/security/FlingWebSecurityConfigurer.java +++ b/service/fling/src/main/java/net/friedl/fling/security/FlingWebSecurityConfigurer.java @@ -53,18 +53,18 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter { .csrf().disable() .cors(withDefaults()) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - // Everybody can try to authenticate .authorizeRequests() .antMatchers("/api/auth/**") .permitAll() .and() - // We need to go from most specific to more general. // Hence, first define user permissions .authorizeRequests() - .antMatchers(HttpMethod.GET, "/api/fling/{flingId}/download") - .hasAuthority(FlingAuthority.FLING_USER.name()) + // TODO: This is still insecure since URLs are not encrypted + // TODO: iframe requests don't send the bearer, use cookie instead + .antMatchers(HttpMethod.GET, "/api/fling/{flingId}/download/{downloadId}") + .permitAll() .and() .authorizeRequests() .antMatchers(HttpMethod.POST, "/api/artifacts/{flingId}/**") @@ -72,13 +72,20 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter { .and() .authorizeRequests() // TODO: This is still insecure since URLs are not encrypted + // TODO: iframe requests don't send the bearer, use cookie instead .antMatchers("/api/artifacts/{artifactId}/{downloadId}/download") .permitAll() .and() .authorizeRequests() + // TODO: Security by request parameters is just not well supported with spring security + // TODO: Change API .regexMatchers(HttpMethod.GET, "\\/api\\/fling\\?(shareId=|flingId=)[a-zA-Z0-9]+") .access("@authorizationService.allowFlingAccess(authentication, request)") .and() + .authorizeRequests() + .antMatchers(HttpMethod.GET, "/api/fling/{flingId}/**") + .access("@authorizationService.allowFlingAccess(#flingId, authentication)") + .and() // And lastly, the owner is allowed everything .authorizeRequests() .antMatchers("/api/**") diff --git a/service/fling/src/main/java/net/friedl/fling/service/FlingService.java b/service/fling/src/main/java/net/friedl/fling/service/FlingService.java index b336d02..6f4dc84 100644 --- a/service/fling/src/main/java/net/friedl/fling/service/FlingService.java +++ b/service/fling/src/main/java/net/friedl/fling/service/FlingService.java @@ -6,12 +6,14 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; +import java.nio.file.Paths; import java.security.MessageDigest; import java.util.Base64; import java.util.List; import java.util.Optional; import java.util.function.Consumer; import java.util.function.Supplier; +import java.util.zip.Deflater; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -127,11 +129,12 @@ public class FlingService { .reduce(0L, (acc, as) -> acc+as); } - public Pair packageFling(Long flingId) throws IOException, ArchiveException { + public String packageFling(Long flingId) throws IOException, ArchiveException { var fling = flingRepository.getOne(flingId); var tempFile = Files.createTempFile(Long.toString(flingId), ".zip"); try(var zipStream = new ZipOutputStream(new FileOutputStream(tempFile.toFile()))){ + zipStream.setLevel(Deflater.BEST_SPEED); for(ArtifactEntity artifactEntity: fling.getArtifacts()) { ZipEntry ze = new ZipEntry(artifactEntity.getName()); zipStream.putNextEntry(ze); @@ -148,8 +151,14 @@ public class FlingService { } } - var archiveLength = tempFile.toFile().length(); - var archiveStream = new FileInputStream(tempFile.toFile()); + return tempFile.getFileName().toString(); + } + + public Pair downloadFling(String fileId) throws IOException, ArchiveException { + var tempFile = Paths.get(System.getProperty("java.io.tmpdir"), fileId).toFile(); + + var archiveLength = tempFile.length(); + var archiveStream = new FileInputStream(tempFile); return Pair.of(archiveStream, archiveLength); } diff --git a/web/fling/src/components/user/DirectDownload.jsx b/web/fling/src/components/user/DirectDownload.jsx new file mode 100644 index 0000000..aec7c78 --- /dev/null +++ b/web/fling/src/components/user/DirectDownload.jsx @@ -0,0 +1,55 @@ +import log from 'loglevel'; +import React, {useRef, useState, useEffect} from 'react'; + +import {flingClient} from '../../util/flingclient'; + +export default function FlingUser(props) { + let iframeContainer = useRef(null); + let [packaging, setPackaging] = useState(true); + let [waitingMessage, setWaitingMessage] = useState(""); + let [downloadUrl, setDownloadUrl] = useState(""); + + useEffect(handleDownload, []); + + function handleDownload() { + flingClient.packageFling(props.fling.id) + .then(downloadUrl => { + setPackaging(false); + // We need this iframe hack because with a regular href, while + // the browser downloads the file fine, it also reloads the page, hence + // loosing all logs and state + let frame = document.createElement("iframe"); + frame.src = downloadUrl; + iframeContainer.current.appendChild(frame); + setDownloadUrl(downloadUrl); + }); + + let randMsg = ["Please stay patient...", + "Your download will be ready soon...", + "Packaging up your files...", + "Almost there..."]; + setInterval(() => setWaitingMessage(randMsg[Math.floor(Math.random() * randMsg.length)]), 10000); + } + + return( +
+
+
+
+ {packaging + ? <>
+ {waitingMessage ? waitingMessage: "Packaging up your files..."} + + : <> +
Your download is ready!
+
+ Download doesn't start?
Click here
+ + } +
+
+
+
+
+ ); +} diff --git a/web/fling/src/components/user/FlingUser.jsx b/web/fling/src/components/user/FlingUser.jsx index 8c64bec..7a118fc 100644 --- a/web/fling/src/components/user/FlingUser.jsx +++ b/web/fling/src/components/user/FlingUser.jsx @@ -5,6 +5,8 @@ import {useParams, BrowserRouter} from 'react-router-dom'; import {flingClient} from '../../util/flingclient'; +import DirectDownload from './DirectDownload'; + export default function FlingUser() { let { shareId } = useParams(); let [fling, setFling] = useState({}); @@ -16,7 +18,7 @@ export default function FlingUser() { return(
- Hello + {fling.sharing && fling.sharing.directDownload ? : ""}
); } diff --git a/web/fling/src/style/fling.scss b/web/fling/src/style/fling.scss index 4f87234..f1710c5 100644 --- a/web/fling/src/style/fling.scss +++ b/web/fling/src/style/fling.scss @@ -302,3 +302,13 @@ tbody td { max-width: 7rem; } } + +/******************\ +| DirectDownload | +\******************/ + +.direct-download-card { + @include shadow; + width: 12rem; + text-align: center; +}