From 4188a86995b0c53980fb0b7709a3d459722cb588 Mon Sep 17 00:00:00 2001 From: MK13 Date: Thu, 20 Oct 2022 21:11:05 +0200 Subject: [PATCH] #22 Password protected shares --- .../main/java/de/nbscloud/files/Share.java | 9 +++ .../files/controller/FilesController.java | 76 +++++++++++++++++-- .../de/nbscloud/files/form/PasswordForm.java | 27 +++++++ .../de/nbscloud/files/form/ShareForm.java | 13 +++- .../resources/i18n/files_messages.properties | 6 ++ .../i18n/files_messages_de_DE.properties | 6 ++ .../main/resources/static/css/files_main.css | 9 ++- .../templates/files/checkPassword.html | 25 ++++++ .../main/resources/templates/files/share.html | 2 + .../nbscloud/webcontainer/MessageHelper.java | 8 +- .../templates/includes/messages.html | 5 ++ 11 files changed, 176 insertions(+), 10 deletions(-) create mode 100644 files/src/main/java/de/nbscloud/files/form/PasswordForm.java create mode 100644 files/src/main/resources/templates/files/checkPassword.html diff --git a/files/src/main/java/de/nbscloud/files/Share.java b/files/src/main/java/de/nbscloud/files/Share.java index fca81db..3c77305 100644 --- a/files/src/main/java/de/nbscloud/files/Share.java +++ b/files/src/main/java/de/nbscloud/files/Share.java @@ -7,6 +7,7 @@ public class Share { private boolean oneTime; private LocalDate expiryDate; private String path; + private String password; // clear text, as protecting it provides no additional security public String getUuid() { return uuid; @@ -39,4 +40,12 @@ public class Share { public void setPath(String path) { this.path = path; } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } } diff --git a/files/src/main/java/de/nbscloud/files/controller/FilesController.java b/files/src/main/java/de/nbscloud/files/controller/FilesController.java index 5f511e8..c1d0574 100644 --- a/files/src/main/java/de/nbscloud/files/controller/FilesController.java +++ b/files/src/main/java/de/nbscloud/files/controller/FilesController.java @@ -8,18 +8,21 @@ import de.nbscloud.files.Share; import de.nbscloud.files.api.FilesService.ContentContainer; import de.nbscloud.files.config.FilesConfig; import de.nbscloud.files.exception.FileSystemServiceException; +import de.nbscloud.files.form.PasswordForm; import de.nbscloud.files.form.RenameForm; import de.nbscloud.files.form.ShareForm; import de.nbscloud.webcontainer.MessageHelper; import de.nbscloud.webcontainer.registry.AppRegistry; import de.nbscloud.webcontainer.shared.config.WebContainerSharedConfig; import org.apache.commons.io.input.ObservableInputStream; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.InputStreamResource; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -148,12 +151,11 @@ public class FilesController implements InitializingBean { final Path targetPath = this.locationTracker.resolve(filename); try { - if(Files.isDirectory(targetPath)) { + if (Files.isDirectory(targetPath)) { downloadDirectory(targetPath, filename, response); return ResponseEntity.ok(":)"); - } - else { + } else { return downloadSingleFile(filename); } } catch (RuntimeException | IOException e) { @@ -218,7 +220,7 @@ public class FilesController implements InitializingBean { @GetMapping("files/share") public String share(Model model, String filename) { 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("filename", filename); this.webContainerSharedConfig.addDefaults(model); @@ -229,7 +231,8 @@ public class FilesController implements InitializingBean { @PostMapping("/files/doShare") public String doShare(@RequestParam("filename") String filename, @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 String shareUuid = UUID.randomUUID().toString(); @@ -240,10 +243,15 @@ public class FilesController implements InitializingBean { .map(ed -> LocalDate.parse(ed, DateTimeFormatter.ofPattern("yyyy-MM-dd"))) .orElse(null); + if(StringUtils.isEmpty(password)) { + password = null; // if no real password has been provided assume no password + } + share.setUuid(shareUuid); share.setPath(filePath.toString()); share.setExpiryDate(expiryDate); share.setOneTime(oneTime); + share.setPassword(password); try { final String shareJson = mapper.writeValueAsString(share); @@ -262,6 +270,64 @@ public class FilesController implements InitializingBean { @GetMapping("files/shares") 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(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(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 { final String shareJson = new String(this.fileSystemService.get(this.locationTracker.resolveShare(shareUuid))); final Share share = mapper.readValue(shareJson, Share.class); diff --git a/files/src/main/java/de/nbscloud/files/form/PasswordForm.java b/files/src/main/java/de/nbscloud/files/form/PasswordForm.java new file mode 100644 index 0000000..56f1175 --- /dev/null +++ b/files/src/main/java/de/nbscloud/files/form/PasswordForm.java @@ -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; + } +} diff --git a/files/src/main/java/de/nbscloud/files/form/ShareForm.java b/files/src/main/java/de/nbscloud/files/form/ShareForm.java index 25cf532..b743cb2 100644 --- a/files/src/main/java/de/nbscloud/files/form/ShareForm.java +++ b/files/src/main/java/de/nbscloud/files/form/ShareForm.java @@ -7,10 +7,13 @@ public class ShareForm { private boolean oneTime; 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.oneTime = oneTime; this.expiryDate = expiryDate; + this.password = password; } public String getFilename() { @@ -36,4 +39,12 @@ public class ShareForm { public void setExpiryDate(LocalDate expiryDate) { this.expiryDate = expiryDate; } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } } diff --git a/files/src/main/resources/i18n/files_messages.properties b/files/src/main/resources/i18n/files_messages.properties index 9d15bc7..d6a5e27 100644 --- a/files/src/main/resources/i18n/files_messages.properties +++ b/files/src/main/resources/i18n/files_messages.properties @@ -20,9 +20,15 @@ nbscloud.files.share.title=nbscloud - files\: share \u0020 nbscloud.files.share.label.filename=Filename\: nbscloud.files.share.label.onetime=One time\: nbscloud.files.share.label.expirydate=Expiry date\: +nbscloud.files.share.label.password=Password\: nbscloud.files.share.submit=Share 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-table.basedir=Base dir\: diff --git a/files/src/main/resources/i18n/files_messages_de_DE.properties b/files/src/main/resources/i18n/files_messages_de_DE.properties index ebee3b3..a581452 100644 --- a/files/src/main/resources/i18n/files_messages_de_DE.properties +++ b/files/src/main/resources/i18n/files_messages_de_DE.properties @@ -20,9 +20,15 @@ nbscloud.files.share.title=nbscloud - Dateien\: teilen \u0020 nbscloud.files.share.label.filename=Dateiname\: nbscloud.files.share.label.onetime=Einmaliger Abruf\: nbscloud.files.share.label.expirydate=Ablaufdatum\: +nbscloud.files.share.label.password=Passwort\: nbscloud.files.share.submit=Teilen 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-table.basedir=Verzeichnis\: diff --git a/files/src/main/resources/static/css/files_main.css b/files/src/main/resources/static/css/files_main.css index 51eae9d..92fe5a8 100644 --- a/files/src/main/resources/static/css/files_main.css +++ b/files/src/main/resources/static/css/files_main.css @@ -49,13 +49,15 @@ } #rename-form, -#share-form { +#share-form, +#share-password-form { margin-top: 3em; margin-left: 3em; } #rename-form *, -#share-form * { +#share-form *, +#share-password-form * { display: block; margin-top: 1em; box-sizing: border-box; @@ -69,7 +71,8 @@ } #rename-form > input[type=text], -#share-form > input[type=text] { +#share-form > input[type=text], +#share-password-form > input[type=text] { width: 70em; } diff --git a/files/src/main/resources/templates/files/checkPassword.html b/files/src/main/resources/templates/files/checkPassword.html new file mode 100644 index 0000000..2dabb84 --- /dev/null +++ b/files/src/main/resources/templates/files/checkPassword.html @@ -0,0 +1,25 @@ + + + + + + <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> \ No newline at end of file diff --git a/files/src/main/resources/templates/files/share.html b/files/src/main/resources/templates/files/share.html index fbfe0ab..c3ef7e1 100644 --- a/files/src/main/resources/templates/files/share.html +++ b/files/src/main/resources/templates/files/share.html @@ -20,6 +20,8 @@ <input type="checkbox" id="oneTime" th:field="*{oneTime}" /> <label for="expiryDate" th:text="#{nbscloud.files.share.label.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}" /> </form> <div th:replace="includes/footer :: footer"/> diff --git a/web-container-registry/src/main/java/de/nbscloud/webcontainer/MessageHelper.java b/web-container-registry/src/main/java/de/nbscloud/webcontainer/MessageHelper.java index a29dc18..f23c115 100644 --- a/web-container-registry/src/main/java/de/nbscloud/webcontainer/MessageHelper.java +++ b/web-container-registry/src/main/java/de/nbscloud/webcontainer/MessageHelper.java @@ -11,7 +11,8 @@ public class MessageHelper { // 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 // 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> infoMessages = new ArrayList<>(); @@ -19,6 +20,7 @@ public class MessageHelper { model.addAttribute("errors", getAndClear(this.errors)); model.addAttribute("infoMessages", getAndClear(this.infoMessages)); model.addAttribute("shareInfo", getAndClear(this.shareInfo)); + model.addAttribute("resolvableErrors", getAndClear(this.resolvableErrors)); } private static List<String> getAndClear(List<String> source) { @@ -33,6 +35,10 @@ public class MessageHelper { this.errors.add(message); } + public void addResolvableError(String message) { + this.resolvableErrors.add(message); + } + public void addShare(String message) { this.shareInfo.add(message); } diff --git a/web-container/src/main/resources/templates/includes/messages.html b/web-container/src/main/resources/templates/includes/messages.html index b25d79a..1545ed2 100644 --- a/web-container/src/main/resources/templates/includes/messages.html +++ b/web-container/src/main/resources/templates/includes/messages.html @@ -10,6 +10,11 @@ <span th:text="${error}"/> </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 th:if="${!infoMessages.isEmpty()}" class="infoMessage message"> <span th:text="#{${infoMessage}}"/>