View Javadoc
1   package net.sumaris.server.http.rest;
2   
3   /*-
4    * #%L
5    * SUMARiS:: Server
6    * %%
7    * Copyright (C) 2018 SUMARiS Consortium
8    * %%
9    * This program is free software: you can redistribute it and/or modify
10   * it under the terms of the GNU General Public License as
11   * published by the Free Software Foundation, either version 3 of the
12   * License, or (at your option) any later version.
13   * 
14   * This program is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17   * GNU General Public License for more details.
18   * 
19   * You should have received a copy of the GNU General Public
20   * License along with this program.  If not, see
21   * <http://www.gnu.org/licenses/gpl-3.0.html>.
22   * #L%
23   */
24  
25  
26  import com.google.common.base.Joiner;
27  import com.google.common.base.Preconditions;
28  import net.sumaris.core.util.Files;
29  import net.sumaris.core.util.StringUtils;
30  import net.sumaris.server.config.SumarisServerConfiguration;
31  import net.sumaris.server.http.MediaTypes;
32  import org.apache.commons.io.FileUtils;
33  import org.slf4j.Logger;
34  import org.slf4j.LoggerFactory;
35  import org.springframework.beans.factory.annotation.Autowired;
36  import org.springframework.core.io.InputStreamResource;
37  import org.springframework.http.HttpHeaders;
38  import org.springframework.http.HttpStatus;
39  import org.springframework.http.MediaType;
40  import org.springframework.http.ResponseEntity;
41  import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
42  import org.springframework.security.core.Authentication;
43  import org.springframework.security.core.context.SecurityContextHolder;
44  import org.springframework.security.core.userdetails.UserDetails;
45  import org.springframework.stereotype.Controller;
46  import org.springframework.web.bind.annotation.PathVariable;
47  import org.springframework.web.bind.annotation.RequestMapping;
48  import org.springframework.web.bind.annotation.RequestParam;
49  
50  import javax.servlet.ServletContext;
51  import java.io.File;
52  import java.io.FileInputStream;
53  import java.io.IOException;
54  
55  @Controller
56  public class DownloadController {
57  
58      /* Logger */
59      private static final Logger log = LoggerFactory.getLogger(DownloadController.class);
60  
61      @Autowired
62      private ServletContext servletContext;
63  
64      @Autowired
65      private SumarisServerConfiguration configuration;
66  
67      @RequestMapping({RestPaths.DOWNLOAD_PATH + "/{username}/{filename}", RestPaths.DOWNLOAD_PATH + "/{filename}"})
68      public ResponseEntity<InputStreamResource> downloadFileAsPath(
69              @PathVariable(name="username", required = false) String username,
70              @PathVariable(name="filename") String filename
71      ) throws IOException {
72          if (StringUtils.isNotBlank(username)) {
73              return doDownloadFile(username + "/" + filename);
74          }
75          else {
76              return doDownloadFile(filename);
77          }
78      }
79  
80      @RequestMapping(RestPaths.DOWNLOAD_PATH)
81      public ResponseEntity<InputStreamResource> downloadFileAsQuery(
82              @RequestParam(name = "username", required = false) String username,
83              @RequestParam(name = "filename") String filename
84      ) throws IOException{
85          if (StringUtils.isNotBlank(username)) {
86              return doDownloadFile(username + "/" + filename);
87          }
88          else {
89              return doDownloadFile(filename);
90          }
91      }
92  
93      public String registerFile(File sourceFile, boolean moveSourceFile) throws IOException {
94  
95          String username = getAuthenticatedUsername();
96  
97          // Make sure the username can be used as a path (fail if '../' injection in the username token)
98          String userPath =  asSecuredPath(username);
99          if (!username.equals(userPath)) {
100             throw new AuthenticationCredentialsNotFoundException("Bad authentication token");
101         }
102 
103         File userDirectory = new File(configuration.getDownloadDirectory(), userPath);
104         FileUtils.forceMkdir(userDirectory);
105         File targetFile = new File(userDirectory, sourceFile.getName());
106 
107         if (targetFile.exists()) {
108             int counter = 1;
109             String baseName = Files.getNameWithoutExtension(sourceFile);
110             String extension = Files.getExtension(sourceFile);
111             do {
112                 targetFile = new File(userDirectory, String.format("%s-%s.%s",
113                         baseName,
114                         counter++,
115                         extension));
116             } while (targetFile.exists());
117         }
118 
119         if (moveSourceFile) {
120             FileUtils.moveFile(sourceFile, targetFile);
121         }
122         else {
123             FileUtils.copyFile(sourceFile, targetFile);
124         }
125 
126         return Joiner.on('/').join(
127                 configuration.getServerUrl() + RestPaths.DOWNLOAD_PATH,
128                 userPath,
129                 targetFile.getName());
130     }
131 
132     /* protected method */
133 
134     protected ResponseEntity<InputStreamResource> doDownloadFile(String filename) throws IOException {
135         if (StringUtils.isBlank(filename)) return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
136 
137         // Avoid '../' in the filename
138         String securedFilename = asSecuredPath(filename);
139         if (!filename.equals(securedFilename)) {
140             log.warn(String.format("Reject download request: invalid path {%s}", filename));
141             return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
142         }
143 
144         MediaType mediaType = MediaTypes.getMediaTypeForFileName(this.servletContext, filename);
145 
146         File file = new File(configuration.getDownloadDirectory(), filename);
147         if (!file.exists()) {
148             log.warn(String.format("Reject download request: file {%s} not found, or invalid path", filename));
149             return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
150         }
151         if (!file.canRead()) {
152             log.warn(String.format("Reject download request: file {%s} not readable", filename));
153             return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
154         }
155 
156         log.debug(String.format("Download request to file {%s} of type {%s}", filename, mediaType));
157         InputStreamResource resource = new InputStreamResource(new FileInputStream(file));
158 
159         return ResponseEntity.ok()
160                 // Content-Disposition
161                 .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + file.getName())
162                 // Content-Type
163                 .contentType(mediaType)
164                 // Content-Length
165                 .contentLength(file.length())
166                 .body(resource);
167     }
168 
169     protected String getAuthenticatedUsername() {
170         Authentication auth = SecurityContextHolder.getContext().getAuthentication();
171         if (auth != null) {
172             Object principal = auth.getPrincipal();
173             if (principal instanceof UserDetails) {
174                 return ((UserDetails) principal).getUsername();
175             }
176         }
177         return null;
178     }
179 
180     protected String asSecuredPath(String path) {
181         Preconditions.checkNotNull(path);
182         Preconditions.checkArgument(path.trim().length() > 0);
183         // Avoid '../' in the filename
184         return path != null ? path.trim().replaceAll("[.][.]/?", "") : null;
185     }
186 }