View Javadoc

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