View Javadoc

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