#22 Password protected shares
This commit is contained in:
@@ -7,6 +7,7 @@ public class Share {
|
|||||||
private boolean oneTime;
|
private boolean oneTime;
|
||||||
private LocalDate expiryDate;
|
private LocalDate expiryDate;
|
||||||
private String path;
|
private String path;
|
||||||
|
private String password; // clear text, as protecting it provides no additional security
|
||||||
|
|
||||||
public String getUuid() {
|
public String getUuid() {
|
||||||
return uuid;
|
return uuid;
|
||||||
@@ -39,4 +40,12 @@ public class Share {
|
|||||||
public void setPath(String path) {
|
public void setPath(String path) {
|
||||||
this.path = path;
|
this.path = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPassword(String password) {
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,18 +8,21 @@ import de.nbscloud.files.Share;
|
|||||||
import de.nbscloud.files.api.FilesService.ContentContainer;
|
import de.nbscloud.files.api.FilesService.ContentContainer;
|
||||||
import de.nbscloud.files.config.FilesConfig;
|
import de.nbscloud.files.config.FilesConfig;
|
||||||
import de.nbscloud.files.exception.FileSystemServiceException;
|
import de.nbscloud.files.exception.FileSystemServiceException;
|
||||||
|
import de.nbscloud.files.form.PasswordForm;
|
||||||
import de.nbscloud.files.form.RenameForm;
|
import de.nbscloud.files.form.RenameForm;
|
||||||
import de.nbscloud.files.form.ShareForm;
|
import de.nbscloud.files.form.ShareForm;
|
||||||
import de.nbscloud.webcontainer.MessageHelper;
|
import de.nbscloud.webcontainer.MessageHelper;
|
||||||
import de.nbscloud.webcontainer.registry.AppRegistry;
|
import de.nbscloud.webcontainer.registry.AppRegistry;
|
||||||
import de.nbscloud.webcontainer.shared.config.WebContainerSharedConfig;
|
import de.nbscloud.webcontainer.shared.config.WebContainerSharedConfig;
|
||||||
import org.apache.commons.io.input.ObservableInputStream;
|
import org.apache.commons.io.input.ObservableInputStream;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.core.io.InputStreamResource;
|
import org.springframework.core.io.InputStreamResource;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
@@ -148,12 +151,11 @@ public class FilesController implements InitializingBean {
|
|||||||
final Path targetPath = this.locationTracker.resolve(filename);
|
final Path targetPath = this.locationTracker.resolve(filename);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if(Files.isDirectory(targetPath)) {
|
if (Files.isDirectory(targetPath)) {
|
||||||
downloadDirectory(targetPath, filename, response);
|
downloadDirectory(targetPath, filename, response);
|
||||||
|
|
||||||
return ResponseEntity.ok(":)");
|
return ResponseEntity.ok(":)");
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
return downloadSingleFile(filename);
|
return downloadSingleFile(filename);
|
||||||
}
|
}
|
||||||
} catch (RuntimeException | IOException e) {
|
} catch (RuntimeException | IOException e) {
|
||||||
@@ -218,7 +220,7 @@ public class FilesController implements InitializingBean {
|
|||||||
@GetMapping("files/share")
|
@GetMapping("files/share")
|
||||||
public String share(Model model, String filename) {
|
public String share(Model model, String filename) {
|
||||||
model.addAttribute("currentLocation", getCurrentLocationPrefixed());
|
model.addAttribute("currentLocation", getCurrentLocationPrefixed());
|
||||||
model.addAttribute("form", new ShareForm(filename, false, LocalDate.now().plusDays(1)));
|
model.addAttribute("form", new ShareForm(filename, false, LocalDate.now().plusDays(1), null));
|
||||||
model.addAttribute("apps", this.appRegistry.getAll());
|
model.addAttribute("apps", this.appRegistry.getAll());
|
||||||
model.addAttribute("filename", filename);
|
model.addAttribute("filename", filename);
|
||||||
this.webContainerSharedConfig.addDefaults(model);
|
this.webContainerSharedConfig.addDefaults(model);
|
||||||
@@ -229,7 +231,8 @@ public class FilesController implements InitializingBean {
|
|||||||
@PostMapping("/files/doShare")
|
@PostMapping("/files/doShare")
|
||||||
public String doShare(@RequestParam("filename") String filename,
|
public String doShare(@RequestParam("filename") String filename,
|
||||||
@RequestParam(value = "oneTime", defaultValue = "false") boolean oneTime,
|
@RequestParam(value = "oneTime", defaultValue = "false") boolean oneTime,
|
||||||
@RequestParam(value = "expiryDate", required = false) String expiryDateString
|
@RequestParam(value = "expiryDate", required = false) String expiryDateString,
|
||||||
|
@RequestParam(value = "password", required = false) String password
|
||||||
) {
|
) {
|
||||||
final Path filePath = this.locationTracker.getRelativeToBaseDir(this.locationTracker.resolve(filename));
|
final Path filePath = this.locationTracker.getRelativeToBaseDir(this.locationTracker.resolve(filename));
|
||||||
final String shareUuid = UUID.randomUUID().toString();
|
final String shareUuid = UUID.randomUUID().toString();
|
||||||
@@ -240,10 +243,15 @@ public class FilesController implements InitializingBean {
|
|||||||
.map(ed -> LocalDate.parse(ed, DateTimeFormatter.ofPattern("yyyy-MM-dd")))
|
.map(ed -> LocalDate.parse(ed, DateTimeFormatter.ofPattern("yyyy-MM-dd")))
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
|
|
||||||
|
if(StringUtils.isEmpty(password)) {
|
||||||
|
password = null; // if no real password has been provided assume no password
|
||||||
|
}
|
||||||
|
|
||||||
share.setUuid(shareUuid);
|
share.setUuid(shareUuid);
|
||||||
share.setPath(filePath.toString());
|
share.setPath(filePath.toString());
|
||||||
share.setExpiryDate(expiryDate);
|
share.setExpiryDate(expiryDate);
|
||||||
share.setOneTime(oneTime);
|
share.setOneTime(oneTime);
|
||||||
|
share.setPassword(password);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final String shareJson = mapper.writeValueAsString(share);
|
final String shareJson = mapper.writeValueAsString(share);
|
||||||
@@ -262,6 +270,64 @@ public class FilesController implements InitializingBean {
|
|||||||
|
|
||||||
@GetMapping("files/shares")
|
@GetMapping("files/shares")
|
||||||
public ResponseEntity shares(String shareUuid) {
|
public ResponseEntity shares(String shareUuid) {
|
||||||
|
try {
|
||||||
|
final String shareJson = new String(this.fileSystemService.get(this.locationTracker.resolveShare(shareUuid)));
|
||||||
|
final Share share = mapper.readValue(shareJson, Share.class);
|
||||||
|
|
||||||
|
if(share.getPassword() != null) {
|
||||||
|
// If there is a password check nothing else, so there is no information leak if the remote doesn't know the password
|
||||||
|
final HttpHeaders headers = new HttpHeaders();
|
||||||
|
|
||||||
|
headers.add("Location", "shares/passwordCheck?shareUuid=" + shareUuid);
|
||||||
|
|
||||||
|
return new ResponseEntity<String>(headers, HttpStatus.FOUND);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return doShares(shareUuid);
|
||||||
|
}
|
||||||
|
} catch (RuntimeException | JsonProcessingException e) {
|
||||||
|
logger.error("Could not get shared file", e);
|
||||||
|
|
||||||
|
return ResponseEntity.internalServerError().body(":(");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("files/shares/passwordCheck")
|
||||||
|
public String passwordCheck(Model model, String shareUuid) {
|
||||||
|
this.messageHelper.addAndClearAll(model);
|
||||||
|
model.addAttribute("form", new PasswordForm(shareUuid, null));
|
||||||
|
this.webContainerSharedConfig.addDefaults(model);
|
||||||
|
|
||||||
|
return "files/checkPassword";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/files/shares/checkPassword")
|
||||||
|
public ResponseEntity checkPassword(@RequestParam(value = "shareUuid", required = true) String shareUuid,
|
||||||
|
@RequestParam(value = "password", required = true) String password) {
|
||||||
|
try {
|
||||||
|
final String shareJson = new String(this.fileSystemService.get(this.locationTracker.resolveShare(shareUuid)));
|
||||||
|
final Share share = mapper.readValue(shareJson, Share.class);
|
||||||
|
|
||||||
|
if(share.getPassword().equals(password)) {
|
||||||
|
return doShares(shareUuid);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.messageHelper.addResolvableError("nbscloud.files.share.error.passwordWrong");
|
||||||
|
|
||||||
|
final HttpHeaders headers = new HttpHeaders();
|
||||||
|
|
||||||
|
headers.add("Location", "passwordCheck?shareUuid=" + shareUuid);
|
||||||
|
|
||||||
|
return new ResponseEntity<String>(headers, HttpStatus.FOUND);
|
||||||
|
}
|
||||||
|
} catch (RuntimeException | JsonProcessingException e) {
|
||||||
|
logger.error("Could not get shared file", e);
|
||||||
|
|
||||||
|
return ResponseEntity.internalServerError().body(":(");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseEntity doShares(String shareUuid) {
|
||||||
try {
|
try {
|
||||||
final String shareJson = new String(this.fileSystemService.get(this.locationTracker.resolveShare(shareUuid)));
|
final String shareJson = new String(this.fileSystemService.get(this.locationTracker.resolveShare(shareUuid)));
|
||||||
final Share share = mapper.readValue(shareJson, Share.class);
|
final Share share = mapper.readValue(shareJson, Share.class);
|
||||||
|
|||||||
27
files/src/main/java/de/nbscloud/files/form/PasswordForm.java
Normal file
27
files/src/main/java/de/nbscloud/files/form/PasswordForm.java
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package de.nbscloud.files.form;
|
||||||
|
|
||||||
|
public class PasswordForm {
|
||||||
|
private String shareUuid;
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
public PasswordForm(String shareUuid, String password) {
|
||||||
|
this.shareUuid = shareUuid;
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getShareUuid() {
|
||||||
|
return shareUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setShareUuid(String shareUuid) {
|
||||||
|
this.shareUuid = shareUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPassword(String password) {
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,10 +7,13 @@ public class ShareForm {
|
|||||||
private boolean oneTime;
|
private boolean oneTime;
|
||||||
private LocalDate expiryDate;
|
private LocalDate expiryDate;
|
||||||
|
|
||||||
public ShareForm(String filename, boolean oneTime, LocalDate expiryDate) {
|
private String password;
|
||||||
|
|
||||||
|
public ShareForm(String filename, boolean oneTime, LocalDate expiryDate, String password) {
|
||||||
this.filename = filename;
|
this.filename = filename;
|
||||||
this.oneTime = oneTime;
|
this.oneTime = oneTime;
|
||||||
this.expiryDate = expiryDate;
|
this.expiryDate = expiryDate;
|
||||||
|
this.password = password;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getFilename() {
|
public String getFilename() {
|
||||||
@@ -36,4 +39,12 @@ public class ShareForm {
|
|||||||
public void setExpiryDate(LocalDate expiryDate) {
|
public void setExpiryDate(LocalDate expiryDate) {
|
||||||
this.expiryDate = expiryDate;
|
this.expiryDate = expiryDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPassword(String password) {
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,15 @@ nbscloud.files.share.title=nbscloud - files\: share \u0020
|
|||||||
nbscloud.files.share.label.filename=Filename\:
|
nbscloud.files.share.label.filename=Filename\:
|
||||||
nbscloud.files.share.label.onetime=One time\:
|
nbscloud.files.share.label.onetime=One time\:
|
||||||
nbscloud.files.share.label.expirydate=Expiry date\:
|
nbscloud.files.share.label.expirydate=Expiry date\:
|
||||||
|
nbscloud.files.share.label.password=Password\:
|
||||||
nbscloud.files.share.submit=Share
|
nbscloud.files.share.submit=Share
|
||||||
|
|
||||||
nbscloud.files.share-message=Shared file\:\u0020
|
nbscloud.files.share-message=Shared file\:\u0020
|
||||||
|
nbscloud.files.share.error.passwordWrong=Wrong password!
|
||||||
|
|
||||||
|
nbscloud.files.share.password.title=Password check
|
||||||
|
nbscloud.files.share.password.submit=Check password
|
||||||
|
nbscloud.files.share.password.label.password=Password\:
|
||||||
|
|
||||||
nbscloud.files.file-disk-usage-widget.heading=files disk usage
|
nbscloud.files.file-disk-usage-widget.heading=files disk usage
|
||||||
nbscloud.files.file-disk-usage-widget-table.basedir=Base dir\:
|
nbscloud.files.file-disk-usage-widget-table.basedir=Base dir\:
|
||||||
|
|||||||
@@ -20,9 +20,15 @@ nbscloud.files.share.title=nbscloud - Dateien\: teilen \u0020
|
|||||||
nbscloud.files.share.label.filename=Dateiname\:
|
nbscloud.files.share.label.filename=Dateiname\:
|
||||||
nbscloud.files.share.label.onetime=Einmaliger Abruf\:
|
nbscloud.files.share.label.onetime=Einmaliger Abruf\:
|
||||||
nbscloud.files.share.label.expirydate=Ablaufdatum\:
|
nbscloud.files.share.label.expirydate=Ablaufdatum\:
|
||||||
|
nbscloud.files.share.label.password=Passwort\:
|
||||||
nbscloud.files.share.submit=Teilen
|
nbscloud.files.share.submit=Teilen
|
||||||
|
|
||||||
nbscloud.files.share-message=Datei geteilt\:\u0020
|
nbscloud.files.share-message=Datei geteilt\:\u0020
|
||||||
|
nbscloud.files.share.error.passwordWrong=Passwort falsch!
|
||||||
|
|
||||||
|
nbscloud.files.share.password.title=Passwort eingeben
|
||||||
|
nbscloud.files.share.password.submit=Passwort pr\u00FCfen
|
||||||
|
nbscloud.files.share.password.label.password=Passwort\:
|
||||||
|
|
||||||
nbscloud.files.file-disk-usage-widget.heading=Dateien Festplattennutzung
|
nbscloud.files.file-disk-usage-widget.heading=Dateien Festplattennutzung
|
||||||
nbscloud.files.file-disk-usage-widget-table.basedir=Verzeichnis\:
|
nbscloud.files.file-disk-usage-widget-table.basedir=Verzeichnis\:
|
||||||
|
|||||||
@@ -49,13 +49,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#rename-form,
|
#rename-form,
|
||||||
#share-form {
|
#share-form,
|
||||||
|
#share-password-form {
|
||||||
margin-top: 3em;
|
margin-top: 3em;
|
||||||
margin-left: 3em;
|
margin-left: 3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#rename-form *,
|
#rename-form *,
|
||||||
#share-form * {
|
#share-form *,
|
||||||
|
#share-password-form * {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -69,7 +71,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#rename-form > input[type=text],
|
#rename-form > input[type=text],
|
||||||
#share-form > input[type=text] {
|
#share-form > input[type=text],
|
||||||
|
#share-password-form > input[type=text] {
|
||||||
width: 70em;
|
width: 70em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
25
files/src/main/resources/templates/files/checkPassword.html
Normal file
25
files/src/main/resources/templates/files/checkPassword.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<title th:text="#{nbscloud.files.share.password.title}"/>
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link th:if="${darkMode}" rel="stylesheet" th:href="@{/css/darkModeColors.css}"/>
|
||||||
|
<link th:if="${!darkMode}" rel="stylesheet" th:href="@{/css/lightModeColors.css}"/>
|
||||||
|
<link rel="stylesheet" th:href="@{/css/main.css}"/>
|
||||||
|
<link rel="stylesheet" th:href="@{/css/files_main.css}"/>
|
||||||
|
<link rel="shortcut icon" th:href="@{/favicon.ico}"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="main-container">
|
||||||
|
<div th:replace="includes/messages :: messages"/>
|
||||||
|
<form id="share-password-form" action="#" th:action="@{/files/shares/checkPassword}" th:object="${form}" method="post" enctype="multipart/form-data">
|
||||||
|
<label for="password" th:text="#{nbscloud.files.share.password.label.password}"/>
|
||||||
|
<input type="password" id="password" th:field="*{password}" />
|
||||||
|
<input type="hidden" id="shareUuid" th:field="*{shareUuid}" />
|
||||||
|
<input type="submit" th:value="#{nbscloud.files.share.password.submit}" />
|
||||||
|
</form>
|
||||||
|
<div th:replace="includes/footer :: footer"/>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -20,6 +20,8 @@
|
|||||||
<input type="checkbox" id="oneTime" th:field="*{oneTime}" />
|
<input type="checkbox" id="oneTime" th:field="*{oneTime}" />
|
||||||
<label for="expiryDate" th:text="#{nbscloud.files.share.label.expirydate}"/>
|
<label for="expiryDate" th:text="#{nbscloud.files.share.label.expirydate}"/>
|
||||||
<input type="date" id="expiryDate" th:field="*{expiryDate}"/>
|
<input type="date" id="expiryDate" th:field="*{expiryDate}"/>
|
||||||
|
<label for="password" th:text="#{nbscloud.files.share.label.password}"/>
|
||||||
|
<input type="text" id="password" th:field="*{password}"/>
|
||||||
<input type="submit" th:value="#{nbscloud.files.share.submit}" />
|
<input type="submit" th:value="#{nbscloud.files.share.submit}" />
|
||||||
</form>
|
</form>
|
||||||
<div th:replace="includes/footer :: footer"/>
|
<div th:replace="includes/footer :: footer"/>
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ public class MessageHelper {
|
|||||||
// We have to temporarily store messages as we redirect: in some methods
|
// We have to temporarily store messages as we redirect: in some methods
|
||||||
// so everything we add to the model will be gone, that's why we store messages
|
// so everything we add to the model will be gone, that's why we store messages
|
||||||
// temporarily in here
|
// temporarily in here
|
||||||
private final List<String> errors = new ArrayList<>();
|
private final List<String> errors = new ArrayList<>(); // not resolved against a bundle
|
||||||
|
private final List<String> resolvableErrors = new ArrayList<>(); // messages that are resolved against a bundle
|
||||||
private final List<String> shareInfo = new ArrayList<>();
|
private final List<String> shareInfo = new ArrayList<>();
|
||||||
private final List<String> infoMessages = new ArrayList<>();
|
private final List<String> infoMessages = new ArrayList<>();
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ public class MessageHelper {
|
|||||||
model.addAttribute("errors", getAndClear(this.errors));
|
model.addAttribute("errors", getAndClear(this.errors));
|
||||||
model.addAttribute("infoMessages", getAndClear(this.infoMessages));
|
model.addAttribute("infoMessages", getAndClear(this.infoMessages));
|
||||||
model.addAttribute("shareInfo", getAndClear(this.shareInfo));
|
model.addAttribute("shareInfo", getAndClear(this.shareInfo));
|
||||||
|
model.addAttribute("resolvableErrors", getAndClear(this.resolvableErrors));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<String> getAndClear(List<String> source) {
|
private static List<String> getAndClear(List<String> source) {
|
||||||
@@ -33,6 +35,10 @@ public class MessageHelper {
|
|||||||
this.errors.add(message);
|
this.errors.add(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addResolvableError(String message) {
|
||||||
|
this.resolvableErrors.add(message);
|
||||||
|
}
|
||||||
|
|
||||||
public void addShare(String message) {
|
public void addShare(String message) {
|
||||||
this.shareInfo.add(message);
|
this.shareInfo.add(message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,11 @@
|
|||||||
<span th:text="${error}"/>
|
<span th:text="${error}"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="messageContainer" th:each="error : ${resolvableErrors}">
|
||||||
|
<div th:if="${!resolvableErrors.isEmpty()}" class="errorMessage message">
|
||||||
|
<span th:text="#{${error}}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="messageContainer" th:each="infoMessage : ${infoMessages}">
|
<div class="messageContainer" th:each="infoMessage : ${infoMessages}">
|
||||||
<div th:if="${!infoMessages.isEmpty()}" class="infoMessage message">
|
<div th:if="${!infoMessages.isEmpty()}" class="infoMessage message">
|
||||||
<span th:text="#{${infoMessage}}"/>
|
<span th:text="#{${infoMessage}}"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user