secure-files

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

commit 5306b59c6c23a303fe46d53b03e8edb690c397e5
Author: Kebigon <git@kebigon.xyz>
Date:   Sun, 10 Oct 2021 10:51:43 +0900

Release v0.1.0

Diffstat:
A.gitignore | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apom.xml | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/assembly/assembly.xml | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/xyz/kebigon/securefiles/DownloadController.java | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/xyz/kebigon/securefiles/IndexController.java | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/xyz/kebigon/securefiles/SecureFilesApplication.java | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/xyz/kebigon/securefiles/WebSecurityConfiguration.java | 47+++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/xyz/kebigon/securefiles/db/DroppedFile.java | 27+++++++++++++++++++++++++++
Asrc/main/java/xyz/kebigon/securefiles/db/DroppedFilesRepository.java | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/packaged-resources/cfg/logback.xml | 18++++++++++++++++++
Asrc/main/packaged-resources/cfg/securefiles_custom.cfg | 9+++++++++
Asrc/main/packaged-resources/cfg/securefiles_required.cfg | 3+++
Asrc/main/resources/application.properties | 3+++
Asrc/main/resources/db/migration/V0_1_0.sql | 8++++++++
Asrc/main/resources/templates/download.mustache | 15+++++++++++++++
Asrc/main/resources/templates/index.mustache | 46++++++++++++++++++++++++++++++++++++++++++++++
Asrc/test/java/xyz/kebigon/securefiles/SecureFilesApplicationTests.java | 13+++++++++++++
17 files changed, 759 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,85 @@ +var/ + +# +# Eclipse +# + +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# Eclipse Core +.project + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ +.apt_generated_test/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + + +# +# Maven +# + +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar diff --git a/pom.xml b/pom.xml @@ -0,0 +1,105 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>xyz.kebigon</groupId> + <artifactId>secure-files</artifactId> + <version>0.1.0</version> + + <parent> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-parent</artifactId> + <version>2.5.4</version> + <relativePath /> <!-- lookup parent from repository --> + </parent> + + <name>Secure files</name> + <description>Demo project for Spring Boot</description> + + <properties> + <java.version>8</java.version> + </properties> + + <dependencies> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter</artifactId> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-jdbc</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-mustache</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-security</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> + </dependency> + + <!-- SQLite JDBC --> + <dependency> + <groupId>org.xerial</groupId> + <artifactId>sqlite-jdbc</artifactId> + </dependency> + <!-- Flyway Core --> + <dependency> + <groupId>org.flywaydb</groupId> + <artifactId>flyway-core</artifactId> + </dependency> + + + <!-- Project Lombok --> + <dependency> + <groupId>org.projectlombok</groupId> + <artifactId>lombok</artifactId> + <scope>provided</scope> + </dependency> + + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <useSystemClassLoader>false</useSystemClassLoader> + <additionalClasspathElements> + <additionalClasspathElement>src/main/packaged-resources/cfg</additionalClasspathElement> + </additionalClasspathElements> + </configuration> + </plugin> + + <plugin> + <artifactId>maven-assembly-plugin</artifactId> + <configuration> + <descriptors> + <descriptor>src/assembly/assembly.xml</descriptor> + </descriptors> + <appendAssemblyId>false</appendAssemblyId> + </configuration> + <executions> + <execution> + <id>make-assembly</id> + <phase>package</phase> + <goals> + <goal>single</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + +</project> diff --git a/src/assembly/assembly.xml b/src/assembly/assembly.xml @@ -0,0 +1,44 @@ +<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd"> + <id>release</id> + + <formats> + <format>tar.gz</format> + </formats> + + <fileSets> + <!-- bin & cfg directories --> + <fileSet> + <directory>src/main/packaged-resources</directory> + <outputDirectory>.</outputDirectory> + <includes> + <include>*/**</include> + </includes> + </fileSet> + <!-- var/log: empty directory for logs --> + <fileSet> + <directory>.</directory> + <outputDirectory>var/log</outputDirectory> + <excludes> + <exclude>*/**</exclude> + </excludes> + </fileSet> + </fileSets> + + <dependencySets> + <!-- lib: my libraries --> + <dependencySet> + <outputDirectory>lib</outputDirectory> + <includes> + <include>xyz.kebigon:*</include> + </includes> + </dependencySet> + <!-- ext: external libraries --> + <dependencySet> + <outputDirectory>ext</outputDirectory> + <excludes> + <exclude>xyz.kebigon:*</exclude> + </excludes> + </dependencySet> + </dependencySets> + +</assembly> diff --git a/src/main/java/xyz/kebigon/securefiles/DownloadController.java b/src/main/java/xyz/kebigon/securefiles/DownloadController.java @@ -0,0 +1,89 @@ +package xyz.kebigon.securefiles; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Controller; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.servlet.ModelAndView; + +import lombok.extern.slf4j.Slf4j; +import xyz.kebigon.securefiles.db.DroppedFile; +import xyz.kebigon.securefiles.db.DroppedFilesRepository; + +@Slf4j +@Controller +@RequestMapping("download") +public class DownloadController +{ + @Autowired + private DroppedFilesRepository repository; + @Value("${filedrop.directory}") + private File directory; + + @GetMapping("/{id}") + public ModelAndView downloadPage(final HttpServletResponse response, @PathVariable final String id) throws SQLException, IOException + { + final DroppedFile droppedFile = repository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + final Path file = Paths.get(directory.getAbsolutePath(), droppedFile.getId()); + + if (droppedFile.isDownloaded() || !Files.exists(file)) + throw new ResponseStatusException(HttpStatus.NOT_FOUND); + + final Map<String, Object> model = new HashMap<String, Object>(); + model.put("file", droppedFile); + return new ModelAndView("download", model); + } + + @GetMapping("/{id}/confirm") + public void download(@PathVariable final String id, final HttpServletRequest request, final HttpServletResponse response) throws IOException, SQLException + { + final DroppedFile droppedFile = repository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + final Path file = Paths.get(directory.getAbsolutePath(), droppedFile.getId()); + + if (droppedFile.isDownloaded() || !Files.exists(file)) + throw new ResponseStatusException(HttpStatus.NOT_FOUND); + + response.setContentType(droppedFile.getContentType()); + response.addHeader("Content-Disposition", "attachment; filename=" + droppedFile.getName()); + + Files.copy(file, response.getOutputStream()); + response.getOutputStream().flush(); + + // TODO: secure delete file + log.info("Deleting file {}...", file); + Files.delete(file); + + droppedFile.setDownloaded(true); + droppedFile.setRemoteAddr(getRemoteAddr(request)); + repository.save(droppedFile); + } + + private static String getRemoteAddr(final HttpServletRequest request) + { + final String forwardedFor = request.getHeader("X-Forwarded-For"); + if (StringUtils.hasText(forwardedFor)) + { + final int commaIdx = forwardedFor.indexOf(','); + return (commaIdx != -1 ? forwardedFor.substring(0, commaIdx) : forwardedFor).trim(); + } + + return request.getRemoteAddr(); + } +} diff --git a/src/main/java/xyz/kebigon/securefiles/IndexController.java b/src/main/java/xyz/kebigon/securefiles/IndexController.java @@ -0,0 +1,80 @@ +package xyz.kebigon.securefiles; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.servlet.ModelAndView; + +import lombok.extern.slf4j.Slf4j; +import xyz.kebigon.securefiles.db.DroppedFile; +import xyz.kebigon.securefiles.db.DroppedFilesRepository; + +@Slf4j +@Controller +public class IndexController +{ + @Value("${filedrop.directory}") + private File directory; + @Autowired + private DroppedFilesRepository repository; + + @GetMapping + public ModelAndView dropFilePage() throws SQLException + { + final Map<String, Object> model = new HashMap<String, Object>(); + model.put("files", repository.findAll()); + return new ModelAndView("index", model); + } + + @PostMapping + public ModelAndView dropFile(@RequestParam("file") final MultipartFile file) throws IllegalStateException, IOException, SQLException + { + final String id = UUID.randomUUID().toString(); + + final File savedFile = new File(directory.getAbsoluteFile(), id); + file.transferTo(savedFile); + + final DroppedFile droppedFile = new DroppedFile(); + droppedFile.setId(id); + droppedFile.setName(file.getOriginalFilename()); + droppedFile.setContentType(file.getContentType()); + droppedFile.setDownloaded(false); + droppedFile.setRemoteAddr(null); + repository.save(droppedFile); + + return dropFilePage(); + } + + @PostMapping(params = "delete") + public ModelAndView delete(@RequestParam("delete") final String id, final ModelMap modelMap) throws IllegalStateException, IOException, SQLException + { + final DroppedFile droppedFile = repository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + + final Path file = Paths.get(directory.getAbsolutePath(), droppedFile.getId()); + if (Files.exists(file)) + { + log.info("Deleting file {}...", file); + Files.delete(file); + } + + repository.deleteById(droppedFile.getId()); + return dropFilePage(); + } +} diff --git a/src/main/java/xyz/kebigon/securefiles/SecureFilesApplication.java b/src/main/java/xyz/kebigon/securefiles/SecureFilesApplication.java @@ -0,0 +1,54 @@ +package xyz.kebigon.securefiles; + +import java.io.File; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.PropertySource; +import org.springframework.core.env.Environment; + +import com.samskivert.mustache.Mustache; +import com.samskivert.mustache.Mustache.TemplateLoader; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@SpringBootApplication +@PropertySource("classpath:securefiles_required.cfg") +@PropertySource("classpath:securefiles_custom.cfg") +public class SecureFilesApplication +{ + public static void main(final String[] args) + { + SpringApplication.run(SecureFilesApplication.class, args); + } + + @Value("${filedrop.directory}") + public void setDirectory(final File directory) + { + createDirectoryIfAbsent(directory); + } + + @Value("${securefiles.database}") + public void setDatabaseFile(final File file) + { + createDirectoryIfAbsent(file.getParentFile()); + } + + private void createDirectoryIfAbsent(final File directory) + { + if (directory.exists()) + return; + + log.info("Creating directory {}", directory); + directory.mkdirs(); + } + + @Bean + public Mustache.Compiler mustacheCompiler(final TemplateLoader mustacheTemplateLoader, final Environment environment) + { + return Mustache.compiler().withLoader(mustacheTemplateLoader).defaultValue("N/A").nullValue("N/A"); + } +} diff --git a/src/main/java/xyz/kebigon/securefiles/WebSecurityConfiguration.java b/src/main/java/xyz/kebigon/securefiles/WebSecurityConfiguration.java @@ -0,0 +1,46 @@ +package xyz.kebigon.securefiles; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +@EnableWebSecurity +public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter +{ + @Value("${spring.security.user.name}") + private String username; + @Value("${spring.security.user.password}") + private String password; + + @Override + protected void configure(final AuthenticationManagerBuilder auth) throws Exception + { + auth.inMemoryAuthentication().withUser(username).password(password).roles("USER"); + } + + @Override + protected void configure(final HttpSecurity http) throws Exception + { + http // + .csrf().disable() // + .authorizeRequests() // + .mvcMatchers("/download/**").anonymous() // + .anyRequest().fullyAuthenticated() // + .and() // + .formLogin().and() // + .httpBasic(); + } + + @Bean + public PasswordEncoder passwordEncoder() + { + return new BCryptPasswordEncoder(); + } +} +\ No newline at end of file diff --git a/src/main/java/xyz/kebigon/securefiles/db/DroppedFile.java b/src/main/java/xyz/kebigon/securefiles/db/DroppedFile.java @@ -0,0 +1,27 @@ +package xyz.kebigon.securefiles.db; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import lombok.Data; + +@Data +public class DroppedFile +{ + private String id; + private String name; + private String contentType; + private boolean downloaded; + private String remoteAddr; + + public static DroppedFile mapToDroppedFile(final ResultSet rs) throws SQLException + { + final DroppedFile droppedFile = new DroppedFile(); + droppedFile.setId(rs.getString("id")); + droppedFile.setName(rs.getString("name")); + droppedFile.setContentType(rs.getString("content_type")); + droppedFile.setDownloaded(rs.getBoolean("downloaded")); + droppedFile.setRemoteAddr(rs.getString("remote_addr")); + return droppedFile; + } +} diff --git a/src/main/java/xyz/kebigon/securefiles/db/DroppedFilesRepository.java b/src/main/java/xyz/kebigon/securefiles/db/DroppedFilesRepository.java @@ -0,0 +1,113 @@ +package xyz.kebigon.securefiles.db; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Optional; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class DroppedFilesRepository +{ + @Autowired + private DataSource dataSource; + + public Iterable<DroppedFile> findAll() throws SQLException + { + try (Connection connection = dataSource.getConnection()) + { + try (final PreparedStatement ps = connection.prepareStatement("SELECT * FROM `dropped_files`")) + { + final ResultSet rs = ps.executeQuery(); + + final Collection<DroppedFile> result = new ArrayList<>(); + while (rs.next()) + result.add(DroppedFile.mapToDroppedFile(rs)); + + log.info("findAll -> {}", result); + return result; + } + } + catch (final Throwable e) + { + log.error("findAll", e); + throw e; + } + } + + public Optional<DroppedFile> findById(final String id) throws SQLException + { + try (Connection connection = dataSource.getConnection()) + { + try (final PreparedStatement ps = connection.prepareStatement("SELECT * FROM `dropped_files` WHERE `id` = ? LIMIT 1")) + { + ps.setString(1, id); + final ResultSet rs = ps.executeQuery(); + + final Optional<DroppedFile> result = rs.next() ? Optional.of(DroppedFile.mapToDroppedFile(rs)) : Optional.empty(); + + log.info("findById: id={} -> {}", id, result); + return result; + } + } + catch (final Throwable e) + { + log.error("findById: id={}", id, e); + throw e; + } + } + + public void save(final DroppedFile entity) throws SQLException + { + try (Connection connection = dataSource.getConnection()) + { + try (final PreparedStatement ps = connection.prepareStatement("REPLACE INTO `dropped_files` VALUES(?, ?, ?, ?, ?)")) + { + ps.setString(1, entity.getId()); + ps.setString(2, entity.getName()); + ps.setString(3, entity.getContentType()); + ps.setBoolean(4, entity.isDownloaded()); + ps.setString(5, entity.getRemoteAddr()); + ps.executeUpdate(); + + log.info("save: entity={}", entity); + return; + } + } + catch (final Throwable e) + { + log.error("save: entity={}", entity, e); + throw e; + } + } + + public void deleteById(final String id) throws SQLException + { + try (Connection connection = dataSource.getConnection()) + { + try (final PreparedStatement ps = connection.prepareStatement("DELETE FROM `dropped_files` WHERE `id` = ?")) + { + ps.setString(1, id); + ps.executeUpdate(); + + log.info("deleteById: id={}", id); + return; + } + } + catch (final Throwable e) + { + log.error("deleteById: id={}", id, e); + throw e; + } + } +} diff --git a/src/main/packaged-resources/cfg/logback.xml b/src/main/packaged-resources/cfg/logback.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration> + <include resource="org/springframework/boot/logging/logback/defaults.xml" /> + + <property name="LOG_FILE" value="var/log/securefiles-app.log" /> + <property name="LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START" value="true"/> + <property name="LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE" value="50MB"/> + <property name="LOGBACK_ROLLINGPOLICY_MAX_HISTORY" value="14"/> + + <include resource="org/springframework/boot/logging/logback/console-appender.xml" /> + <include resource="org/springframework/boot/logging/logback/file-appender.xml" /> + + <root level="INFO"> + <appender-ref ref="CONSOLE" /> + <appender-ref ref="FILE" /> + </root> +</configuration> +\ No newline at end of file diff --git a/src/main/packaged-resources/cfg/securefiles_custom.cfg b/src/main/packaged-resources/cfg/securefiles_custom.cfg @@ -0,0 +1,8 @@ +securefiles.database=var/db/database.db +filedrop.directory=var/files + +spring.datasource.url=jdbc:sqlite:file:${securefiles.database} + +# bcrypt password generated with: htpasswd -nBC 17 "" | tr -d ':' +spring.security.user.name=user +spring.security.user.password=$2y$10$qAhct7NUlQ0/UIps7CL2hO3S3kwjy1ymKc8np2mY643QxAKZvs6h. +\ No newline at end of file diff --git a/src/main/packaged-resources/cfg/securefiles_required.cfg b/src/main/packaged-resources/cfg/securefiles_required.cfg @@ -0,0 +1,2 @@ +server.address=127.0.0.1 +server.port=26777 +\ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties @@ -0,0 +1,2 @@ +spring.servlet.multipart.max-file-size=-1 +spring.servlet.multipart.max-request-size=-1 +\ No newline at end of file diff --git a/src/main/resources/db/migration/V0_1_0.sql b/src/main/resources/db/migration/V0_1_0.sql @@ -0,0 +1,7 @@ +CREATE TABLE `dropped_files` ( + `id` VARCHAR(11) PRIMARY KEY, + `name` VARCHAR(255) NOT NULL, + `content_type` VARCHAR(255) NOT NULL, + `downloaded` BOOLEAN DEFAULT 'FALSE', + `remote_addr` VARCHAR(45) DEFAULT NULL +); +\ No newline at end of file diff --git a/src/main/resources/templates/download.mustache b/src/main/resources/templates/download.mustache @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> + <head> + </head> + <body style="text-align: center"> + <h1>Download {{file.name}}</h1> + <p>You're about to download <b>{{file.name}}</b> (type {{file.contentType}}).</p> + <p style="color: red">Please note that for security reasons, the below button will work only once, {{file.name}} will be deleted from the server.</p> + + <form method="get" action="/download/{{id}}/confirm"> + <input type="submit" value="Download" /> + </form> + </body> +</html> +\ No newline at end of file diff --git a/src/main/resources/templates/index.mustache b/src/main/resources/templates/index.mustache @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<html> + <head> + </head> + <body> + <div> + <form method="post" enctype="multipart/form-data"> + <input type="file" name="file" /> + <input type="submit" /> + </form> + </div> + + <table class="table table-striped table-sm"> + <thead> + <tr> + <th scope="col">File</th> + <th scope="col">Content-type</th> + <th scope="col">Status</th> + <th scope="col">Downloader IP address</th> + </tr> + </thead> + <tbody> +{{#files}} + <tr> + <td> + {{#downloaded}}{{name}}{{/downloaded}} + {{^downloaded}}<a href="/download/{{id}}">{{name}}</a>{{/downloaded}} + </td> + <td>{{contentType}}</td> + <td> + {{#downloaded}}Downloaded{{/downloaded}} + </td> + <td>{{remoteAddr}}</td> + <td> + <form method="post"> + <input type="hidden" name="delete" value="{{id}}"> + <input type="submit" value="Delete" /> + </form> + </td> + </tr> +{{/files}} + </tbody> + </table> + </body> +</html> +\ No newline at end of file diff --git a/src/test/java/xyz/kebigon/securefiles/SecureFilesApplicationTests.java b/src/test/java/xyz/kebigon/securefiles/SecureFilesApplicationTests.java @@ -0,0 +1,13 @@ +package xyz.kebigon.securefiles; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SecureFilesApplicationTests +{ + @Test + void contextLoads() + { + } +}