1 // 2 // ======================================================================== 3 // Copyright (c) 1995-2014 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 /* ------------------------------------------------------------ */ init(FilterConfig config)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 /* ------------------------------------------------------------ */ getInitBoolean(FilterConfig config,String name)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 /* ------------------------------------------------------------ */ doFilter(ServletRequest req, ServletResponse res, FilterChain chain)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 /* ------------------------------------------------------------ */ isHidden(String pathInContext)175 private boolean isHidden(String pathInContext) 176 { 177 return _hidden.containsKey(pathInContext); 178 } 179 180 /* ------------------------------------------------------------ */ destroy()181 public void destroy() 182 { 183 } 184 185 /* ------------------------------------------------------------------- */ handlePut(HttpServletRequest request, HttpServletResponse response, String pathInContext, File file)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 /* ------------------------------------------------------------------- */ handleDelete(HttpServletRequest request, HttpServletResponse response, String pathInContext, File file)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 /* ------------------------------------------------------------------- */ handleMove(HttpServletRequest request, HttpServletResponse response, String pathInContext, File file)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 /* ------------------------------------------------------------ */ handleOptions(FilterChain chain, HttpServletRequest request, HttpServletResponse response)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 */ passConditionalHeaders(HttpServletRequest request, HttpServletResponse response, File file)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 } 375