View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.servlets;
20  
21  import java.io.File;
22  import java.io.FileOutputStream;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.OutputStream;
26  import java.net.URI;
27  import java.net.URISyntaxException;
28  import java.util.Arrays;
29  import java.util.HashSet;
30  import java.util.Set;
31  import java.util.concurrent.ConcurrentHashMap;
32  import java.util.concurrent.ConcurrentMap;
33  
34  import javax.servlet.Filter;
35  import javax.servlet.FilterChain;
36  import javax.servlet.FilterConfig;
37  import javax.servlet.ServletContext;
38  import javax.servlet.ServletException;
39  import javax.servlet.ServletRequest;
40  import javax.servlet.ServletResponse;
41  import javax.servlet.UnavailableException;
42  import javax.servlet.http.HttpServletRequest;
43  import javax.servlet.http.HttpServletResponse;
44  import javax.servlet.http.HttpServletResponseWrapper;
45  
46  import org.eclipse.jetty.util.IO;
47  import org.eclipse.jetty.util.StringUtil;
48  import org.eclipse.jetty.util.URIUtil;
49  
50  /**
51   * PutFilter
52   *
53   * A Filter that handles PUT, DELETE and MOVE methods.
54   * Files are hidden during PUT operations, so that 404's result.
55   *
56   * The following init parameters pay be used:<ul>
57   * <li><b>baseURI</b> - The file URI of the document root for put content.
58   * <li><b>delAllowed</b> - boolean, if true DELETE and MOVE methods are supported.
59   * <li><b>putAtomic</b> - boolean, if true PUT files are written to a temp location and moved into place.
60   * </ul>
61   *
62   */
63  public class PutFilter implements Filter
64  {
65      public final static String __PUT="PUT";
66      public final static String __DELETE="DELETE";
67      public final static String __MOVE="MOVE";
68      public final static String __OPTIONS="OPTIONS";
69  
70      Set<String> _operations = new HashSet<String>();
71      private ConcurrentMap<String,String> _hidden = new ConcurrentHashMap<String, String>();
72  
73      private ServletContext _context;
74      private String _baseURI;
75      private boolean _delAllowed;
76      private boolean _putAtomic;
77      private File _tmpdir;
78  
79  
80      /* ------------------------------------------------------------ */
81      public void init(FilterConfig config) throws ServletException
82      {
83          _context=config.getServletContext();
84  
85          _tmpdir=(File)_context.getAttribute("javax.servlet.context.tempdir");
86  
87          if (_context.getRealPath("/")==null)
88             throw new UnavailableException("Packed war");
89  
90          String b = config.getInitParameter("baseURI");
91          if (b != null)
92          {
93              _baseURI=b;
94          }
95          else
96          {
97              File base=new File(_context.getRealPath("/"));
98              _baseURI=base.toURI().toString();
99          }
100 
101         _delAllowed = getInitBoolean(config,"delAllowed");
102         _putAtomic = getInitBoolean(config,"putAtomic");
103 
104         _operations.add(__OPTIONS);
105         _operations.add(__PUT);
106         if (_delAllowed)
107         {
108             _operations.add(__DELETE);
109             _operations.add(__MOVE);
110         }
111     }
112 
113     /* ------------------------------------------------------------ */
114     private boolean getInitBoolean(FilterConfig config,String name)
115     {
116         String value = config.getInitParameter(name);
117         return value != null && value.length() > 0 && (value.startsWith("t") || value.startsWith("T") || value.startsWith("y") || value.startsWith("Y") || value.startsWith("1"));
118     }
119 
120     /* ------------------------------------------------------------ */
121     public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException
122     {
123         HttpServletRequest request=(HttpServletRequest)req;
124         HttpServletResponse response=(HttpServletResponse)res;
125 
126         String servletPath =request.getServletPath();
127         String pathInfo = request.getPathInfo();
128         String pathInContext = URIUtil.addPaths(servletPath, pathInfo);
129 
130         String resource = URIUtil.addPaths(_baseURI,pathInContext);
131 
132         String method = request.getMethod();
133         boolean op = _operations.contains(method);
134 
135         if (op)
136         {
137             File file = null;
138             try
139             {
140                 if (method.equals(__OPTIONS))
141                     handleOptions(chain,request, response);
142                 else
143                 {
144                     file=new File(new URI(resource));
145                     boolean exists = file.exists();
146                     if (exists && !passConditionalHeaders(request, response, file))
147                         return;
148 
149                     if (method.equals(__PUT))
150                         handlePut(request, response,pathInContext, file);
151                     else if (method.equals(__DELETE))
152                         handleDelete(request, response, pathInContext, file);
153                     else if (method.equals(__MOVE))
154                         handleMove(request, response, pathInContext, file);
155                     else
156                         throw new IllegalStateException();
157                 }
158             }
159             catch(Exception e)
160             {
161                 _context.log(e.toString(),e);
162                 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
163             }
164         }
165         else
166         {
167             if (isHidden(pathInContext))
168                 response.sendError(HttpServletResponse.SC_NOT_FOUND);
169             else
170                 chain.doFilter(request,response);
171             return;
172         }
173     }
174 
175     /* ------------------------------------------------------------ */
176     private boolean isHidden(String pathInContext)
177     {
178         return _hidden.containsKey(pathInContext);
179     }
180 
181     /* ------------------------------------------------------------ */
182     public void destroy()
183     {
184     }
185 
186     /* ------------------------------------------------------------------- */
187     public void handlePut(HttpServletRequest request, HttpServletResponse response, String pathInContext, File file) throws ServletException, IOException
188     {
189         boolean exists = file.exists();
190         if (pathInContext.endsWith("/"))
191         {
192             if (!exists)
193             {
194                 if (!file.mkdirs())
195                     response.sendError(HttpServletResponse.SC_FORBIDDEN);
196                 else
197                 {
198                     response.setStatus(HttpServletResponse.SC_CREATED);
199                     response.flushBuffer();
200                 }
201             }
202             else
203             {
204                 response.setStatus(HttpServletResponse.SC_OK);
205                 response.flushBuffer();
206             }
207         }
208         else
209         {
210             boolean ok=false;
211             try
212             {
213                 _hidden.put(pathInContext,pathInContext);
214                 File parent = file.getParentFile();
215                 parent.mkdirs();
216                 int toRead = request.getContentLength();
217                 InputStream in = request.getInputStream();
218 
219 
220                 if (_putAtomic)
221                 {
222                     File tmp=File.createTempFile(file.getName(),null,_tmpdir);
223                     try (OutputStream out = new FileOutputStream(tmp,false))
224                     {
225                         if (toRead >= 0)
226                             IO.copy(in, out, toRead);
227                         else
228                             IO.copy(in, out);
229                     }
230 
231                     if (!tmp.renameTo(file))
232                         throw new IOException("rename from "+tmp+" to "+file+" failed");
233                 }
234                 else
235                 {
236                     try (OutputStream out = new FileOutputStream(file,false))
237                     {
238                         if (toRead >= 0)
239                             IO.copy(in, out, toRead);
240                         else
241                             IO.copy(in, out);
242                     }
243                 }
244 
245                 response.setStatus(exists ? HttpServletResponse.SC_OK : HttpServletResponse.SC_CREATED);
246                 response.flushBuffer();
247                 ok=true;
248             }
249             catch (Exception ex)
250             {
251                 _context.log(ex.toString(),ex);
252                 response.sendError(HttpServletResponse.SC_FORBIDDEN);
253             }
254             finally
255             {
256                 if (!ok)
257                 {
258                     try
259                     {
260                         if (file.exists())
261                             file.delete();
262                     }
263                     catch(Exception e)
264                     {
265                         _context.log(e.toString(),e);
266                     }
267                 }
268                 _hidden.remove(pathInContext);
269             }
270         }
271     }
272 
273     /* ------------------------------------------------------------------- */
274     public void handleDelete(HttpServletRequest request, HttpServletResponse response, String pathInContext, File file) throws ServletException, IOException
275     {
276         try
277         {
278             // delete the file
279             if (file.delete())
280             {
281                 response.setStatus(HttpServletResponse.SC_NO_CONTENT);
282                 response.flushBuffer();
283             }
284             else
285                 response.sendError(HttpServletResponse.SC_FORBIDDEN);
286         }
287         catch (SecurityException sex)
288         {
289             _context.log(sex.toString(),sex);
290             response.sendError(HttpServletResponse.SC_FORBIDDEN);
291         }
292     }
293 
294     /* ------------------------------------------------------------------- */
295     public void handleMove(HttpServletRequest request, HttpServletResponse response, String pathInContext, File file)
296         throws ServletException, IOException, URISyntaxException
297     {
298         String newPath = URIUtil.canonicalPath(request.getHeader("new-uri"));
299         if (newPath == null)
300         {
301             response.sendError(HttpServletResponse.SC_BAD_REQUEST);
302             return;
303         }
304 
305         String contextPath = request.getContextPath();
306         if (contextPath != null && !newPath.startsWith(contextPath))
307         {
308             response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
309             return;
310         }
311         String newInfo = newPath;
312         if (contextPath != null)
313             newInfo = newInfo.substring(contextPath.length());
314 
315         String new_resource = URIUtil.addPaths(_baseURI,newInfo);
316         File new_file=new File(new URI(new_resource));
317 
318         file.renameTo(new_file);
319 
320         response.setStatus(HttpServletResponse.SC_NO_CONTENT);
321         response.flushBuffer();
322     }
323 
324     /* ------------------------------------------------------------ */
325     public void handleOptions(FilterChain chain, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
326     {
327         chain.doFilter(request,new HttpServletResponseWrapper(response)
328         {
329             @Override
330             public void setHeader(String name, String value)
331             {
332                 if ("Allow".equalsIgnoreCase(name))
333                 {
334                     Set<String> options = new HashSet<String>();
335                     options.addAll(Arrays.asList(StringUtil.csvSplit(value)));
336                     options.addAll(_operations);
337                     value=null;
338                     for (String o : options)
339                         value=value==null?o:(value+", "+o);
340                 }
341 
342                 super.setHeader(name,value);
343             }
344         });
345 
346     }
347 
348     /* ------------------------------------------------------------ */
349     /*
350      * Check modification date headers.
351      */
352     protected boolean passConditionalHeaders(HttpServletRequest request, HttpServletResponse response, File file) throws IOException
353     {
354         long date = 0;
355 
356         if ((date = request.getDateHeader("if-unmodified-since")) > 0)
357         {
358             if (file.lastModified() / 1000 > date / 1000)
359             {
360                 response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
361                 return false;
362             }
363         }
364 
365         if ((date = request.getDateHeader("if-modified-since")) > 0)
366         {
367             if (file.lastModified() / 1000 <= date / 1000)
368             {
369                 response.reset();
370                 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
371                 response.flushBuffer();
372                 return false;
373             }
374         }
375         return true;
376     }
377 }