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                     try (OutputStream out = new FileOutputStream(tmp,false))
223                     {
224                         if (toRead >= 0)
225                             IO.copy(in, out, toRead);
226                         else
227                             IO.copy(in, out);
228                     }
229 
230                     if (!tmp.renameTo(file))
231                         throw new IOException("rename from "+tmp+" to "+file+" failed");
232                 }
233                 else
234                 {
235                     try (OutputStream out = new FileOutputStream(file,false))
236                     {
237                         if (toRead >= 0)
238                             IO.copy(in, out, toRead);
239                         else
240                             IO.copy(in, out);
241                     }
242                 }
243 
244                 response.setStatus(exists ? HttpServletResponse.SC_OK : HttpServletResponse.SC_CREATED);
245                 response.flushBuffer();
246                 ok=true;
247             }
248             catch (Exception ex)
249             {
250                 _context.log(ex.toString(),ex);
251                 response.sendError(HttpServletResponse.SC_FORBIDDEN);
252             }
253             finally
254             {
255                 if (!ok)
256                 {
257                     try
258                     {
259                         if (file.exists())
260                             file.delete();
261                     }
262                     catch(Exception e)
263                     {
264                         _context.log(e.toString(),e);
265                     }
266                 }
267                 _hidden.remove(pathInContext);
268             }
269         }
270     }
271 
272     /* ------------------------------------------------------------------- */
273     public void handleDelete(HttpServletRequest request, HttpServletResponse response, String pathInContext, File file) throws ServletException, IOException
274     {
275         try
276         {
277             // delete the file
278             if (file.delete())
279             {
280                 response.setStatus(HttpServletResponse.SC_NO_CONTENT);
281                 response.flushBuffer();
282             }
283             else
284                 response.sendError(HttpServletResponse.SC_FORBIDDEN);
285         }
286         catch (SecurityException sex)
287         {
288             _context.log(sex.toString(),sex);
289             response.sendError(HttpServletResponse.SC_FORBIDDEN);
290         }
291     }
292 
293     /* ------------------------------------------------------------------- */
294     public void handleMove(HttpServletRequest request, HttpServletResponse response, String pathInContext, File file)
295         throws ServletException, IOException, URISyntaxException
296     {
297         String newPath = URIUtil.canonicalPath(request.getHeader("new-uri"));
298         if (newPath == null)
299         {
300             response.sendError(HttpServletResponse.SC_BAD_REQUEST);
301             return;
302         }
303 
304         String contextPath = request.getContextPath();
305         if (contextPath != null && !newPath.startsWith(contextPath))
306         {
307             response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
308             return;
309         }
310         String newInfo = newPath;
311         if (contextPath != null)
312             newInfo = newInfo.substring(contextPath.length());
313 
314         String new_resource = URIUtil.addPaths(_baseURI,newInfo);
315         File new_file=new File(new URI(new_resource));
316 
317         file.renameTo(new_file);
318 
319         response.setStatus(HttpServletResponse.SC_NO_CONTENT);
320         response.flushBuffer();
321     }
322 
323     /* ------------------------------------------------------------ */
324     public void handleOptions(FilterChain chain, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
325     {
326         chain.doFilter(request,new HttpServletResponseWrapper(response)
327         {
328             @Override
329             public void setHeader(String name, String value)
330             {
331                 if ("Allow".equalsIgnoreCase(name))
332                 {
333                     Set<String> options = new HashSet<String>();
334                     options.addAll(Arrays.asList(value.split(" *, *")));
335                     options.addAll(_operations);
336                     value=null;
337                     for (String o : options)
338                         value=value==null?o:(value+", "+o);
339                 }
340 
341                 super.setHeader(name,value);
342             }
343         });
344 
345     }
346 
347     /* ------------------------------------------------------------ */
348     /*
349      * Check modification date headers.
350      */
351     protected boolean passConditionalHeaders(HttpServletRequest request, HttpServletResponse response, File file) throws IOException
352     {
353         long date = 0;
354 
355         if ((date = request.getDateHeader("if-unmodified-since")) > 0)
356         {
357             if (file.lastModified() / 1000 > date / 1000)
358             {
359                 response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
360                 return false;
361             }
362         }
363 
364         if ((date = request.getDateHeader("if-modified-since")) > 0)
365         {
366             if (file.lastModified() / 1000 <= date / 1000)
367             {
368                 response.reset();
369                 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
370                 response.flushBuffer();
371                 return false;
372             }
373         }
374         return true;
375     }
376 }