diff --git a/README.md b/README.md index b745c7d..01c98b1 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ files: |[application.properties](./web-container/src/main/resources/config/application.properties)|Main config file providing general app properties| |[shared-application.properties](./web-container-config/src/main/resources/config/shared-application.properties)|Properties shared by all apps| |[files-application.properties](./files/src/main/resources/config/files-application.properties)|Config file for the files app| +|[dashboard-application.properties](./dashboard/src/main/resources/config/dashboard-application.properties)|Config file for the dashboard app| ## Apache httpd config It is advised to not expose NoBullShit-cloud directly - instead a proxy server like Apache httpd should be used to shield access. diff --git a/files/src/main/java/de/nbscloud/files/AppLocationTracker.java b/files/src/main/java/de/nbscloud/files/AppLocationTracker.java index c84b62d..553b77b 100644 --- a/files/src/main/java/de/nbscloud/files/AppLocationTracker.java +++ b/files/src/main/java/de/nbscloud/files/AppLocationTracker.java @@ -6,10 +6,12 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.SessionScope; import java.nio.file.Path; @Component +@SessionScope public class AppLocationTracker implements InitializingBean { private static final Logger logger = LoggerFactory.getLogger(AppLocationTracker.class); diff --git a/files/src/main/java/de/nbscloud/files/FileSystemService.java b/files/src/main/java/de/nbscloud/files/FileSystemService.java index 7c73de9..65d511a 100644 --- a/files/src/main/java/de/nbscloud/files/FileSystemService.java +++ b/files/src/main/java/de/nbscloud/files/FileSystemService.java @@ -403,6 +403,12 @@ public class FileSystemService { } } + public boolean isDirectory(Path path) { + this.locationTracker.ensureValidPath(path); + + return Files.isDirectory(path); + } + public List list(SortOrder sortOrder) { return list(this.locationTracker.getCurrentLocation(), sortOrder, path -> this.locationTracker.getRelativeToBaseDir(path)); } diff --git a/files/src/main/java/de/nbscloud/files/FilesServiceImpl.java b/files/src/main/java/de/nbscloud/files/FilesServiceImpl.java index 06a66a2..5635825 100644 --- a/files/src/main/java/de/nbscloud/files/FilesServiceImpl.java +++ b/files/src/main/java/de/nbscloud/files/FilesServiceImpl.java @@ -5,12 +5,14 @@ import de.nbscloud.files.exception.FileSystemServiceException; import de.nbscloud.webcontainer.registry.App; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.web.context.annotation.SessionScope; import java.nio.file.Path; import java.util.List; import java.util.Optional; @Service +@SessionScope public class FilesServiceImpl implements FilesService { @Autowired private FileSystemService fileSystemService; diff --git a/files/src/main/java/de/nbscloud/files/LocationTracker.java b/files/src/main/java/de/nbscloud/files/LocationTracker.java index daf2e95..b6344d3 100644 --- a/files/src/main/java/de/nbscloud/files/LocationTracker.java +++ b/files/src/main/java/de/nbscloud/files/LocationTracker.java @@ -5,12 +5,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.SessionScope; import java.nio.file.Path; import java.nio.file.Paths; @Component +@SessionScope public class LocationTracker implements InitializingBean { private static final Logger logger = LoggerFactory.getLogger(LocationTracker.class); @@ -31,11 +35,11 @@ public class LocationTracker implements InitializingBean { this.baseDirPath = Paths.get(this.filesConfig.getBaseDir()); this.currentLocation = this.baseDirPath.resolve(""); - logger.info("Initialized location to {}", this.currentLocation); + logger.info("{}: Initialized location to {}", id(), this.currentLocation); } public void reset() { - logger.debug("Reset location"); + logger.debug("{}: Reset location", id()); try { afterPropertiesSet(); @@ -44,6 +48,14 @@ public class LocationTracker implements InitializingBean { } } + public void resetCurrentLocation() { + this.currentLocation = this.baseDirPath.resolve(""); + } + + private long id() { + return System.identityHashCode(this); + } + Path getCurrentLocation() { return this.currentLocation; } @@ -79,7 +91,19 @@ public class LocationTracker implements InitializingBean { public void setCurrentLocation(String navigateTo) { validate_internal(navigateTo); - this.currentLocation = this.baseDirPath.resolve(navigateTo); + final Path newLocation = this.baseDirPath.resolve(navigateTo); + + logger.debug("{}: Change location from {} to {}", id(), this.currentLocation, newLocation); + + this.currentLocation = newLocation; + } + + public void setBaseDirPath(String path) { + validate_internal(path); + + this.baseDirPath = this.baseDirPath.resolve(path); + + logger.debug("{}: Change base dir to {}", id(), baseDirPath); } private void validate_internal(String navigateTo) { diff --git a/files/src/main/java/de/nbscloud/files/SessionInfo.java b/files/src/main/java/de/nbscloud/files/SessionInfo.java new file mode 100644 index 0000000..6ab175b --- /dev/null +++ b/files/src/main/java/de/nbscloud/files/SessionInfo.java @@ -0,0 +1,18 @@ +package de.nbscloud.files; + +import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.SessionScope; + +@Component +@SessionScope +public class SessionInfo { + private boolean restrictedSession = false; + + public boolean isRestrictedSession() { + return restrictedSession; + } + + public void setRestrictedSession(boolean restrictedSession) { + this.restrictedSession = restrictedSession; + } +} 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 4524558..b32218a 100644 --- a/files/src/main/java/de/nbscloud/files/controller/FilesController.java +++ b/files/src/main/java/de/nbscloud/files/controller/FilesController.java @@ -12,6 +12,7 @@ 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.files.SessionInfo; import de.nbscloud.webcontainer.registry.AppRegistry; import de.nbscloud.webcontainer.shared.config.WebContainerSharedConfig; import org.apache.commons.io.input.ObservableInputStream; @@ -22,7 +23,6 @@ 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; @@ -37,7 +37,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; @@ -45,7 +44,6 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -70,6 +68,8 @@ public class FilesController implements InitializingBean { private ObjectMapper mapper; @Autowired private MessageHelper messageHelper; + @Autowired + private SessionInfo sessionInfo; @GetMapping("/files/browse/**") public String start(Model model, HttpServletRequest httpServletRequest, String sortOrder) { @@ -82,6 +82,7 @@ public class FilesController implements InitializingBean { model.addAttribute("content", getContent(order)); model.addAttribute("sortOrder", order); model.addAttribute("apps", this.appRegistry.getAll()); + model.addAttribute("restricted", this.sessionInfo.isRestrictedSession()); this.webContainerSharedConfig.addDefaults(model); return "files/filesIndex"; @@ -119,6 +120,7 @@ public class FilesController implements InitializingBean { model.addAttribute("targetDirs", this.fileSystemService.collectDirs(filename)); model.addAttribute("apps", this.appRegistry.getAll()); model.addAttribute("filename", filename); + model.addAttribute("restricted", this.sessionInfo.isRestrictedSession()); this.webContainerSharedConfig.addDefaults(model); return "files/rename"; @@ -223,6 +225,7 @@ public class FilesController implements InitializingBean { model.addAttribute("form", new ShareForm(filename, false, LocalDate.now().plusDays(1), null)); model.addAttribute("apps", this.appRegistry.getAll()); model.addAttribute("filename", filename); + model.addAttribute("restricted", this.sessionInfo.isRestrictedSession()); this.webContainerSharedConfig.addDefaults(model); return "files/share"; @@ -269,21 +272,39 @@ public class FilesController implements InitializingBean { } @GetMapping("files/shares") - public ResponseEntity shares(String shareUuid) { + public Object shares(Model model, String shareUuid) { + this.locationTracker.reset(); + try { final String shareJson = new String(this.fileSystemService.get(this.locationTracker.resolveShare(shareUuid))); final Share share = mapper.readValue(shareJson, Share.class); + final Path sharedFilePath = this.locationTracker.resolveToBaseDir(share.getPath()); + final Optional optExpiryDate = Optional.ofNullable(share.getExpiryDate()); + final boolean expired = optExpiryDate.map(expiryDate -> LocalDate.now().isAfter(expiryDate)).orElse(false); + + this.sessionInfo.setRestrictedSession(true); 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(); + model.addAttribute("form", new PasswordForm(shareUuid, null)); + this.webContainerSharedConfig.addDefaults(model); - headers.add("Location", "shares/passwordCheck?shareUuid=" + shareUuid); - - return new ResponseEntity(headers, HttpStatus.FOUND); + return "files/checkPassword"; } else { - return doShares(shareUuid); + if (expired) { + fileSystemService.delete(locationTracker.resolveShare(shareUuid)); + + return ResponseEntity.status(410).body("Share expired!"); + } + + if(this.fileSystemService.isDirectory(sharedFilePath)) { + this.locationTracker.setBaseDirPath(share.getPath()); + + return "redirect:/files/browse"; + } + else { + return doShares(shareUuid); + } } } catch (RuntimeException | JsonProcessingException e) { logger.error("Could not get shared file", e); @@ -292,33 +313,40 @@ public class FilesController implements InitializingBean { } } - @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, + public Object checkPassword(Model model, @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); + final Path sharedFilePath = this.locationTracker.resolveToBaseDir(share.getPath()); + final Optional optExpiryDate = Optional.ofNullable(share.getExpiryDate()); + final boolean expired = optExpiryDate.map(expiryDate -> LocalDate.now().isAfter(expiryDate)).orElse(false); if(share.getPassword().equals(password)) { - return doShares(shareUuid); + if (expired) { + fileSystemService.delete(locationTracker.resolveShare(shareUuid)); + + return ResponseEntity.status(410).body("Share expired!"); + } + + if(this.fileSystemService.isDirectory(sharedFilePath)) { + this.locationTracker.setBaseDirPath(share.getPath()); + + return "redirect:/files/browse"; + } + else { + return doShares(shareUuid); + } } else { this.messageHelper.addResolvableError("nbscloud.files.share.error.passwordWrong"); + this.messageHelper.addAndClearAll(model); - final HttpHeaders headers = new HttpHeaders(); + model.addAttribute("form", new PasswordForm(shareUuid, null)); + this.webContainerSharedConfig.addDefaults(model); - headers.add("Location", "passwordCheck?shareUuid=" + shareUuid); - - return new ResponseEntity(headers, HttpStatus.FOUND); + return "files/checkPassword"; } } catch (RuntimeException | JsonProcessingException e) { logger.error("Could not get shared file", e); @@ -333,13 +361,9 @@ public class FilesController implements InitializingBean { final Share share = mapper.readValue(shareJson, Share.class); final Path sharedFilePath = this.locationTracker.resolveToBaseDir(share.getPath()); final String filename = sharedFilePath.getFileName().toString(); - final Optional optExpiryDate = Optional.ofNullable(share.getExpiryDate()); - final boolean expired = optExpiryDate.map(expiryDate -> LocalDate.now().isAfter(expiryDate)).orElse(false); - if (expired) { - fileSystemService.delete(locationTracker.resolveShare(shareUuid)); - - return ResponseEntity.status(410).body("Share expired!"); + if(this.fileSystemService.isDirectory(sharedFilePath)) { + throw new IllegalStateException("Attempt to access folder via file share: " + sharedFilePath); } return ResponseEntity.ok() @@ -381,6 +405,7 @@ public class FilesController implements InitializingBean { model.addAttribute("files", files); model.addAttribute("apps", this.appRegistry.getAll()); + model.addAttribute("restricted", this.sessionInfo.isRestrictedSession()); this.webContainerSharedConfig.addDefaults(model); return "files/gallery"; @@ -415,7 +440,12 @@ public class FilesController implements InitializingBean { if (split.length > 1) { this.locationTracker.setCurrentLocation(UriUtils.decode(split[1].substring(1), StandardCharsets.UTF_8)); } else { - this.locationTracker.reset(); + if(!this.sessionInfo.isRestrictedSession()) { + this.locationTracker.reset(); + } + else { + this.locationTracker.resetCurrentLocation(); + } } } @@ -437,23 +467,23 @@ public class FilesController implements InitializingBean { @Override public void afterPropertiesSet() throws Exception { - if (this.filesConfig.isUseTrashBin()) { - try { - this.fileSystemService.createDirectory(this.filesConfig.getTrashBinName()); - } catch (FileSystemServiceException e) { - if (!FileAlreadyExistsException.class.equals(e.getCause().getClass())) { - throw e; - } - // else: do nothing - } - } - try { - this.fileSystemService.createDirectory(this.filesConfig.getSharesName()); - } catch (FileSystemServiceException e) { - if (!FileAlreadyExistsException.class.equals(e.getCause().getClass())) { - throw e; - } - // else: do nothing - } +// if (this.filesConfig.isUseTrashBin()) { +// try { +// this.fileSystemService.createDirectory(this.filesConfig.getTrashBinName()); +// } catch (FileSystemServiceException e) { +// if (!FileAlreadyExistsException.class.equals(e.getCause().getClass())) { +// throw e; +// } +// // else: do nothing +// } +// } +// try { +// this.fileSystemService.createDirectory(this.filesConfig.getSharesName()); +// } catch (FileSystemServiceException e) { +// if (!FileAlreadyExistsException.class.equals(e.getCause().getClass())) { +// throw e; +// } +// // else: do nothing +// } } } diff --git a/files/src/main/resources/templates/files/filesIndex.html b/files/src/main/resources/templates/files/filesIndex.html index b332a12..c098858 100644 --- a/files/src/main/resources/templates/files/filesIndex.html +++ b/files/src/main/resources/templates/files/filesIndex.html @@ -49,9 +49,9 @@ d - - -
-
+ - -
+ diff --git a/files/src/main/resources/templates/files/includes/menu.html b/files/src/main/resources/templates/files/includes/menu.html index fcdf2c1..7181e33 100644 --- a/files/src/main/resources/templates/files/includes/menu.html +++ b/files/src/main/resources/templates/files/includes/menu.html @@ -1,6 +1,6 @@