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