Direct download

Implement direct download with online packaging
This commit is contained in:
Armin Friedl 2020-05-31 14:41:04 +02:00
parent 4618cc9bff
commit c1171e8376
Signed by: armin
GPG key ID: 48C726EEE7FBCBC8
7 changed files with 104 additions and 13 deletions

View file

@ -69,11 +69,15 @@ public class FlingController {
flingService.deleteFlingById(flingId); 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") @GetMapping(path = "/fling/{flingId}/download/{downloadId}")
public ResponseEntity<Resource> downloadFling(@PathVariable Long flingId) throws ArchiveException, IOException { public ResponseEntity<Resource> downloadFling(@PathVariable Long flingId, @PathVariable String downloadId) throws ArchiveException, IOException {
var fling = flingService.findFlingById(flingId).orElseThrow(); var fling = flingService.findFlingById(flingId).orElseThrow();
var flingPackage = flingService.packageFling(flingId); var flingPackage = flingService.downloadFling(downloadId);
var stream = new InputStreamResource(flingPackage.getFirst()); var stream = new InputStreamResource(flingPackage.getFirst());
return ResponseEntity.ok() return ResponseEntity.ok()

View file

@ -29,8 +29,12 @@ public class AuthorizationService {
return userAuth.getShareId().equals(shareUrl); return userAuth.getShareId().equals(shareUrl);
} }
public boolean allowFlingAccess(Long flingId) { public boolean allowFlingAccess(Long flingId, FlingToken authentication) {
return false; if(authentication.getGrantedFlingAuthority().getAuthority().equals(FlingAuthority.FLING_OWNER.name())) {
return true;
}
return authentication.getGrantedFlingAuthority().getFlingId().equals(flingId);
} }
public boolean allowFlingAccess(FlingToken authentication, HttpServletRequest request) { public boolean allowFlingAccess(FlingToken authentication, HttpServletRequest request) {

View file

@ -53,18 +53,18 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
.csrf().disable() .csrf().disable()
.cors(withDefaults()) .cors(withDefaults())
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
// Everybody can try to authenticate // Everybody can try to authenticate
.authorizeRequests() .authorizeRequests()
.antMatchers("/api/auth/**") .antMatchers("/api/auth/**")
.permitAll() .permitAll()
.and() .and()
// We need to go from most specific to more general. // We need to go from most specific to more general.
// Hence, first define user permissions // Hence, first define user permissions
.authorizeRequests() .authorizeRequests()
.antMatchers(HttpMethod.GET, "/api/fling/{flingId}/download") // TODO: This is still insecure since URLs are not encrypted
.hasAuthority(FlingAuthority.FLING_USER.name()) // TODO: iframe requests don't send the bearer, use cookie instead
.antMatchers(HttpMethod.GET, "/api/fling/{flingId}/download/{downloadId}")
.permitAll()
.and() .and()
.authorizeRequests() .authorizeRequests()
.antMatchers(HttpMethod.POST, "/api/artifacts/{flingId}/**") .antMatchers(HttpMethod.POST, "/api/artifacts/{flingId}/**")
@ -72,13 +72,20 @@ public class FlingWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
.and() .and()
.authorizeRequests() .authorizeRequests()
// TODO: This is still insecure since URLs are not encrypted // 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") .antMatchers("/api/artifacts/{artifactId}/{downloadId}/download")
.permitAll() .permitAll()
.and() .and()
.authorizeRequests() .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]+") .regexMatchers(HttpMethod.GET, "\\/api\\/fling\\?(shareId=|flingId=)[a-zA-Z0-9]+")
.access("@authorizationService.allowFlingAccess(authentication, request)") .access("@authorizationService.allowFlingAccess(authentication, request)")
.and() .and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/api/fling/{flingId}/**")
.access("@authorizationService.allowFlingAccess(#flingId, authentication)")
.and()
// And lastly, the owner is allowed everything // And lastly, the owner is allowed everything
.authorizeRequests() .authorizeRequests()
.antMatchers("/api/**") .antMatchers("/api/**")

View file

@ -6,12 +6,14 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.Base64; import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
@ -127,11 +129,12 @@ public class FlingService {
.reduce(0L, (acc, as) -> acc+as); .reduce(0L, (acc, as) -> acc+as);
} }
public Pair<InputStream, Long> packageFling(Long flingId) throws IOException, ArchiveException { public String packageFling(Long flingId) throws IOException, ArchiveException {
var fling = flingRepository.getOne(flingId); var fling = flingRepository.getOne(flingId);
var tempFile = Files.createTempFile(Long.toString(flingId), ".zip"); var tempFile = Files.createTempFile(Long.toString(flingId), ".zip");
try(var zipStream = new ZipOutputStream(new FileOutputStream(tempFile.toFile()))){ try(var zipStream = new ZipOutputStream(new FileOutputStream(tempFile.toFile()))){
zipStream.setLevel(Deflater.BEST_SPEED);
for(ArtifactEntity artifactEntity: fling.getArtifacts()) { for(ArtifactEntity artifactEntity: fling.getArtifacts()) {
ZipEntry ze = new ZipEntry(artifactEntity.getName()); ZipEntry ze = new ZipEntry(artifactEntity.getName());
zipStream.putNextEntry(ze); zipStream.putNextEntry(ze);
@ -148,8 +151,14 @@ public class FlingService {
} }
} }
var archiveLength = tempFile.toFile().length(); return tempFile.getFileName().toString();
var archiveStream = new FileInputStream(tempFile.toFile()); }
public Pair<InputStream, Long> 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); return Pair.of(archiveStream, archiveLength);
} }

View file

@ -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(
<div>
<div className="container-center">
<div className="card direct-download-card">
<div className="card-body ">
{packaging
? <><div className="loading loading-lg" />
{waitingMessage ? waitingMessage: "Packaging up your files..."}
</>
: <>
<h5>Your download is <span className="text-primary">ready!</span></h5>
<i className="icon icon-check icon-2x text-primary" /><br/>
<span className="text-dark">Download doesn't start? <br/><a href={downloadUrl}>Click here</a></span>
</>
}
</div>
</div>
</div>
<div className="d-hide" ref={iframeContainer} />
</div>
);
}

View file

@ -5,6 +5,8 @@ import {useParams, BrowserRouter} from 'react-router-dom';
import {flingClient} from '../../util/flingclient'; import {flingClient} from '../../util/flingclient';
import DirectDownload from './DirectDownload';
export default function FlingUser() { export default function FlingUser() {
let { shareId } = useParams(); let { shareId } = useParams();
let [fling, setFling] = useState({}); let [fling, setFling] = useState({});
@ -16,7 +18,7 @@ export default function FlingUser() {
return( return(
<div> <div>
Hello {fling.sharing && fling.sharing.directDownload ? <DirectDownload fling={fling} />: ""}
</div> </div>
); );
} }

View file

@ -302,3 +302,13 @@ tbody td {
max-width: 7rem; max-width: 7rem;
} }
} }
/******************\
| DirectDownload |
\******************/
.direct-download-card {
@include shadow;
width: 12rem;
text-align: center;
}