diff src/nabble/view/lib/JtpContextServlet.java @ 0:7ecd1a4ef557

add content
author Franklin Schmidt <fschmidt@gmail.com>
date Thu, 21 Mar 2019 19:15:52 -0600
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/nabble/view/lib/JtpContextServlet.java	Thu Mar 21 19:15:52 2019 -0600
@@ -0,0 +1,796 @@
+package nabble.view.lib;
+
+import fschmidt.util.servlet.*;
+import fschmidt.util.java.ProcUtils;
+import fschmidt.util.java.SimpleClassLoader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+
+
+public final class JtpContextServlet extends HttpServlet implements JtpContext {
+	private static final Logger logger = LoggerFactory.getLogger(JtpContextServlet.class);
+
+	private static final Set<String> allowedMethods = new HashSet<String>(Arrays.asList(
+		"GET", "POST", "HEAD"
+	));
+	private String base;
+	private boolean reload = false;
+	private boolean recompile = false;
+	private SimpleClassLoader.Filter filter = null;
+	private ClassLoader cl = null;
+	private Map<String,HttpServlet> map = new HashMap<String,HttpServlet>();
+	private long clTime;
+	private Object lock = new Object();
+	private HttpCache httpCache;
+	private boolean isCaching;
+	private String characterEncoding;
+	private Map<String, String> customHeaders = new HashMap<String, String>();
+	private UrlMapper urlMapper = new UrlMapper() {
+		public UrlMapping getUrlMapping(HttpServletRequest request) {
+			return null;
+		}
+	};
+	private Set<String> errorCache = null;
+	private Collection<String> ipList = null;
+	private static final String authKeyAttr = "authKey";
+	private static final String[] noModifyingEvents = new String[]{"_"};
+
+	public void setUrlMapper(UrlMapper urlMapper) {
+		this.urlMapper = urlMapper;
+	}
+
+	public HttpCache getHttpCache() {
+		return httpCache;
+	}
+
+	public void setHttpCache(HttpCache httpCache) {
+		this.httpCache = httpCache;
+	}
+
+	public void addCustomHeader(String key, String value) {
+		this.customHeaders.put(key, value);
+	}
+
+	public void unloadServlets() {
+		if( !reload )
+			throw new UnsupportedOperationException("'reload' must be set");
+		synchronized(lock) {
+			cl = new SimpleClassLoader(filter);
+			map = new HashMap<String,HttpServlet>();
+			clTime = System.currentTimeMillis();
+		}
+	}
+
+	public void setBase(String base) {
+		if( base==null )
+			throw new NullPointerException();
+		this.base = base;
+	}
+
+	public void init()
+		throws ServletException
+	{
+		ServletContext context = getServletContext();
+		String newBase = getInitParameter("base");
+		if( newBase != null )
+			setBase(newBase);
+		recompile = Boolean.valueOf(getInitParameter("recompile"));
+		reload = recompile || Boolean.valueOf(getInitParameter("reload"));
+		if( reload ) {
+			filter = new SimpleClassLoader.Filter(){
+				final String s = base + ".";
+				public boolean load(String className)  {
+					return className.startsWith(s);
+				}
+			};
+			unloadServlets();
+		}
+		context.setAttribute(JtpContext.attrName,this);
+		String servletS = getInitParameter("servlet");
+		if( servletS != null ) {
+			throw new RuntimeException("the 'servlet' init parameter is no longer supported");
+		}
+		isCaching = "true".equalsIgnoreCase(getInitParameter("cache"));
+		if( isCaching ) {
+			if( httpCache==null ) {
+				logger.error("can't set init parameter 'cache' to true without httpCache");
+				System.exit(-1);
+			}
+			logger.info("cache");
+		}
+		characterEncoding = getInitParameter("characterEncoding");
+		{
+			String s = getInitParameter("timeLimit");
+			if( s != null )
+				timeLimit = Long.parseLong(s);
+		}
+		{
+			String s = getInitParameter("errorCacheSize");
+			if( s != null ) {
+				final int errorCacheSize = Integer.parseInt(s);
+				errorCache = Collections.synchronizedSet(Collections.newSetFromMap(new LinkedHashMap<String,Boolean>(){
+					protected boolean removeEldestEntry(Map.Entry eldest) {
+						return size() > errorCacheSize;
+					}
+				}));
+			}
+		}
+		{
+			String s = getInitParameter("ipListSize");
+			if( s != null ) {
+				final int ipListSize = Integer.parseInt(s);
+				ipList = Collections.synchronizedList(new LinkedList<String>() {
+					public boolean add(String s) {
+						if( contains(s) )
+							return false;
+						super.add(s);
+						if( size() > ipListSize )
+							removeFirst();
+						return true;
+					}
+				});
+			}
+		}
+	}
+
+	private boolean isInErrorCache(String s) {
+		return errorCache==null || !errorCache.add(s);
+	}
+
+	private boolean isInIpList(String ip) {
+		return ipList!=null && !ipList.add(ip);
+	}
+
+	public static interface DestroyListener {
+		public void destroyed();
+	}
+
+	private DestroyListener destroyListener = null;
+
+	public void addDestroyListener(DestroyListener dl) {
+		synchronized(lock) {
+			if( destroyListener!=null )
+				throw new RuntimeException("only one DestroyListener allowed");
+			destroyListener = dl;
+		}
+	}
+
+	public void destroy() {
+		synchronized(lock) {
+			if( destroyListener != null )
+				destroyListener.destroyed();
+		}
+	}
+
+	public static final class RequestAndResponse {
+		public final HttpServletRequest request;
+		public final HttpServletResponse response;
+
+		public RequestAndResponse(HttpServletRequest request,HttpServletResponse response) {
+			this.request = request;
+			this.response = response;
+		}
+	}
+
+	public static interface CustomWrappers {
+		public RequestAndResponse wrap(HttpServletRequest request, HttpServletResponse response);
+	}
+
+	private CustomWrappers customWrappers;
+
+	public void setCustomWrappers(CustomWrappers customWrappers) {
+		this.customWrappers = customWrappers;
+	}
+
+	private static String hideNull(String s) {
+		return s==null ? "" : s;
+	}
+
+	private String getServletPath(HttpServletRequest request) {
+		return request.getServletPath() + hideNull(request.getPathInfo());
+	}
+
+	protected void service(HttpServletRequest request,HttpServletResponse response)
+		throws ServletException, IOException
+	{
+		final TimeLimit tl = startTimeLimit(request);
+		response = new HttpServletResponseWrapper(response) {
+			PrintWriter writer = null;
+			ServletOutputStream out = null;
+
+			public PrintWriter getWriter()
+				throws java.io.IOException
+			{
+				if( writer==null ) {
+					writer = new PrintWriter(super.getWriter()) {
+						public void write(String s,int off,int len) {
+							long t = System.currentTimeMillis();
+							super.write(s,off,len);
+							tl.ioTime += System.currentTimeMillis() - t;
+						}
+						public void write(char[] buf,int off,int len) {
+							long t = System.currentTimeMillis();
+							super.write(buf,off,len);
+							tl.ioTime += System.currentTimeMillis() - t;
+						}
+						public void write(int c) {
+							long t = System.currentTimeMillis();
+							super.write(c);
+							tl.ioTime += System.currentTimeMillis() - t;
+						}
+						public void flush() {
+							long t = System.currentTimeMillis();
+							super.flush();
+							tl.ioTime += System.currentTimeMillis() - t;
+						}
+						public void println() {
+							long t = System.currentTimeMillis();
+							super.println();
+							tl.ioTime += System.currentTimeMillis() - t;
+						}
+					};
+				}
+				return writer;
+			}
+
+			public ServletOutputStream getOutputStream()
+				throws java.io.IOException
+			{
+				if( out==null ) {
+					final ServletOutputStream sos = super.getOutputStream();
+					out = new ServletOutputStream() {
+						public void write(byte[] b,int off,int len) throws IOException {
+							long t = System.currentTimeMillis();
+							sos.write(b,off,len);
+							tl.ioTime += System.currentTimeMillis() - t;
+						}
+						public void write(byte[] b) throws IOException {
+							long t = System.currentTimeMillis();
+							sos.write(b);
+							tl.ioTime += System.currentTimeMillis() - t;
+						}
+						public void write(int c) throws IOException {
+							long t = System.currentTimeMillis();
+							sos.write(c);
+							tl.ioTime += System.currentTimeMillis() - t;
+						}
+						public void flush() throws IOException {
+							long t = System.currentTimeMillis();
+							sos.flush();
+							tl.ioTime += System.currentTimeMillis() - t;
+						}
+					};
+				}
+				return out;
+			}
+
+			public void sendError(int sc) throws IOException {
+				long t = System.currentTimeMillis();
+				super.sendError(sc);
+				tl.ioTime += System.currentTimeMillis() - t;
+			}
+
+			public void sendRedirect(String location) throws IOException {
+				if( containsHeader("Expires") )
+					setHeader("Expires",null);
+				if( containsHeader("Last-Modified") )
+					setHeader("Last-Modified",null);
+				if( containsHeader("Etag") )
+					setHeader("Etag",null);
+				if( containsHeader("Cache-Control") )
+					setHeader("Cache-Control",null);
+				if( containsHeader("Content-Type") )
+					setHeader("Content-Type",null);
+				if( containsHeader("Content-Length") )
+//					setHeader("Content-Length",null);
+					setContentLength(0);
+				super.sendRedirect(location);
+			}
+		};
+		service2(request,response);
+		checkTimeLimit(request);
+	}
+
+	private void service2(HttpServletRequest request, HttpServletResponse response)
+		throws ServletException, IOException
+	{
+		if( !allowedMethods.contains(request.getMethod()) ) {
+			response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+			return;
+		}
+//		String contextPath = request.getContextPath();
+//		String contextUrl = ServletUtils.getContextURL(request);
+
+		// First we set the character encoding because any manipulation
+		// of request parameters will break without this.
+		response.setHeader("Content-Type","text/html; charset=utf-8");  // default, servlet can override
+		if( characterEncoding != null ) {
+			response.setCharacterEncoding(characterEncoding);
+			request.setCharacterEncoding(characterEncoding);
+		}
+
+		HttpServlet servlet;
+		String path = getServletPath(request);
+
+		UrlMapping urlMapping = urlMapper.getUrlMapping(request);
+		if( urlMapping != null ) {
+			try {
+				servlet = getServletFromClass(urlMapping.servletClass.getName());
+			} catch(ClassNotFoundException e) {
+				throw new RuntimeException(e);
+			}
+			final Map params = urlMapping.parameterMap;
+			request = new BetterRequestWrapper(request) {
+				public Map getParameterMap() {
+					return params;
+				}
+			};
+		} else {
+			try {
+				servlet = getServlet(path);
+			} catch(ClassNotFoundException e) {
+				response.sendError(HttpServletResponse.SC_NOT_FOUND);
+				String agent = request.getHeader("user-agent");
+				String referer = request.getHeader("referer");
+				String remote = getClientIpAddr(request);
+				String msg = request.getRequestURL()+" referer="+referer+" user-agent="+agent+" remote="+remote;
+				if( referer==null ) {
+					logger.info(msg,e);
+				} else {
+					logger.warn(msg,e);
+				}
+				return;
+			}
+		}
+
+		// Custom headers
+		addCustomHeaders(response);
+
+		AuthorizingServlet auth = servlet instanceof AuthorizingServlet ? (AuthorizingServlet)servlet : null;
+		if( isCaching ) {
+			String etagS = request.getHeader("If-None-Match");
+			if( etagS != null ) {
+				String prevEtag = null;
+				for( String etag : etagS.split("\\s*,\\s*") ) {
+					if( etag.equals(prevEtag) )
+						continue;
+					prevEtag = etag;
+					if( etag.length()>=2 && etag.charAt(0)=='"' && etag.charAt(etag.length()-1)=='"' )
+						etag = etag.substring(1,etag.length()-1);
+					String authKey = null;
+					if( etag.length()>=2 && etag.charAt(0)=='[' ) {
+						int i = etag.indexOf(']');
+						if( i > 0 ) {
+							if( auth != null )
+								authKey = etag.substring(1,i);
+							etag = etag.substring(i+1);
+						}
+					}
+					String[] events = etag.split("~");
+					long lastModified = getLastModified(events);
+					try {
+						if( lastModified <= request.getDateHeader("If-Modified-Since") ) {
+							if( authKey==null || authorize(auth,authKey,request,response) )
+								response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
+							return;
+						}
+					} catch(RuntimeException e) {
+						handleException(request,e);
+					}
+				}
+			}
+		}
+		String authKey = auth==null ? null : getAuthorizationKey(auth,request);
+		if( authKey != null ) {
+			if( !authorize(auth,authKey,request,response) )
+				return;
+			request.setAttribute(authKeyAttr,authKey);
+		}
+
+		if( servlet instanceof CanonicalUrl ) {
+			CanonicalUrl srv = (CanonicalUrl)servlet;
+			StringBuffer currentUrl = request.getRequestURL();
+			int i = currentUrl.indexOf(";");
+			if( i != -1 )
+				currentUrl.setLength(i);
+			String query = request.getQueryString();
+			if( query != null )
+				currentUrl.append( '?' ).append( query );
+			try {
+				String canonicalUrl = srv.getCanonicalUrl(request);
+				if( canonicalUrl != null && !stripScheme(currentUrl.toString()).equals(stripScheme(canonicalUrl)) ) {
+					response.setHeader("Location",canonicalUrl);
+					response.sendError( HttpServletResponse.SC_MOVED_PERMANENTLY );
+					return;
+				}
+			} catch(RuntimeException e) {
+				logger.warn("couldn't get canonical url",e);
+			}
+		}
+
+		request.setAttribute("servlet",servlet);
+
+		try {
+			if (customWrappers != null) {
+				RequestAndResponse rr = customWrappers.wrap(request, response);
+				request = rr.request;
+				response = rr.response;
+			}
+			servlet.service(request,response);
+		} catch(RuntimeException e) {
+			handleException(request,e);
+		} catch(ServletException e) {
+			handleException(request,e);
+		}
+	}
+
+	private static String stripScheme(String url) {
+		return url.substring(url.indexOf(':'));
+	}
+
+	public void setEtag( HttpServletRequest request, HttpServletResponse response, String... modifyingEvents ) {
+		if( modifyingEvents.length == 0 )
+			modifyingEvents = noModifyingEvents;
+		StringBuilder buf = new StringBuilder();
+		String authKey = (String)request.getAttribute(authKeyAttr);
+		if( authKey != null )
+			buf.append( '[' ).append( authKey).append( ']' );
+		buf.append( modifyingEvents[0] );
+		for( int i=1; i<modifyingEvents.length; i++ ) {
+			buf.append( '~' ).append( modifyingEvents[i] );
+		}
+		response.setHeader("Etag",buf.toString());
+		long lastModified = getLastModified(modifyingEvents);
+		response.setDateHeader("Last-Modified",lastModified);
+		response.setHeader("Cache-Control","max-age=0");
+	}
+
+	private boolean authorize(AuthorizingServlet auth,String authKey,HttpServletRequest request,HttpServletResponse response)
+		throws IOException, ServletException
+	{
+		try {
+			if (customWrappers != null) {
+				RequestAndResponse rr = customWrappers.wrap(request, response);
+				request = rr.request;
+				response = rr.response;
+			}
+			return auth.authorize(authKey,request,response);
+		} catch(RuntimeException e) {
+			handleException(request,e);
+		} catch(ServletException e) {
+			handleException(request,e);
+		}
+		throw new RuntimeException("never");
+	}
+
+	private String getAuthorizationKey(AuthorizingServlet auth,HttpServletRequest request)
+		throws ServletException
+	{
+		try {
+			return auth.getAuthorizationKey(request);
+		} catch(RuntimeException e) {
+			handleException(request,e);
+		} catch(ServletException e) {
+			handleException(request,e);
+		}
+		return null;  // never gets here
+	}
+
+	private long getLastModified(String[] modifyingEvents) {
+		long[] lastModifieds = httpCache.lastModifieds(modifyingEvents);
+		long lastModified = lastModifieds[0];
+		for( int i=1; i<lastModifieds.length; i++ ) {
+			long lm = lastModifieds[i];
+			if( lastModified < lm )
+				lastModified = lm;
+		}
+		return lastModified;
+	}
+
+	/** Adds all custom headers to the response object. */
+	private void addCustomHeaders(HttpServletResponse response) {
+		Set<Map.Entry<String, String>> entries = this.customHeaders.entrySet();
+		for (Map.Entry<String, String> entry : entries) {
+			response.setHeader(entry.getKey(), entry.getValue());
+		}
+	}
+
+	private void handleException(HttpServletRequest request,RuntimeException e)
+		throws ServletException
+	{
+		JtpRuntimeException rte;
+		try {
+			String agent = request.getHeader("user-agent");
+			if( agent == null )
+				throw new JtpServletException(request,"null agent",e);
+			if (!isValidAgent(agent))
+				throw new JtpServletException(request, "bad agent " + agent, e);
+			String remote = getClientIpAddr(request);
+			String referer = request.getHeader("referer");
+			StringBuilder buf = new StringBuilder()
+				.append( "method=" ).append( request.getMethod() )
+				.append( " user-agent=" ).append( agent )
+				.append( " referer=" ).append( referer )
+				.append( " remote=" ).append( remote )
+			;
+			String etag = request.getHeader("If-None-Match");
+			if( etag != null )
+				buf.append( " etag=[" ).append( etag ).append( "]" );
+			if( referer==null || isInIpList(remote) )
+				throw new JtpServletException(request,buf.toString(),e);
+			rte = new JtpRuntimeException(request,buf.toString(),e);
+		} catch(RuntimeException e2) {
+			logger.error("failed to handle",e);
+			throw e2;
+		}
+		throw rte;
+	}
+
+	private static void handleException(HttpServletRequest request,ServletException e)
+		throws ServletException
+	{
+		String agent = request.getHeader("user-agent");
+		throw new JtpServletException(request,"user-agent="+agent+" method="+request.getMethod()+" referer="+request.getHeader("referer"),e);
+	}
+
+	private static class JtpRuntimeException extends RuntimeException {
+		private JtpRuntimeException(HttpServletRequest request,String msg,RuntimeException e) {
+			super("url="+getCurrentURL(request)+"  "+msg,e);
+		}
+	}
+
+	private static class JtpServletException extends ServletException {
+		private JtpServletException(HttpServletRequest request,String msg,Exception e) {
+			super("url="+getCurrentURL(request)+"  "+msg,e);
+		}
+	}
+
+	// work-around jetty bug
+	private static String getCurrentURL(HttpServletRequest request) {
+		try {
+			return ServletUtils.getCurrentURL(request);
+		} catch(RuntimeException e) {
+			logger.warn("jetty screwed up",e);
+			return "[failed]";
+		}
+	}
+
+	private static boolean isValidAgent(String agent) {
+		if (agent == null)
+			return false;
+		for (String badAgent : badAgents) {
+			if (agent.indexOf(badAgent) >= 0)
+				return false;
+		}
+		return true;
+	}
+
+	private static final String[] badAgents = new String[]{
+		"MJ12bot",
+		"WISEnutbot",
+		"Win98",  // not worth handling these
+		"Windows 98",
+		"Windows 95",
+		"RixBot",
+		"User-Agent",  // from corrupt header
+		"Firefox/0",  // ancient version of Firefox
+		"Firefox/2.",  // ancient version of Firefox
+		"Firefox/3.",  // ancient version of Firefox
+		"Opera 7.",  // ancient version of Opera
+		"Opera/7.",
+		"Opera 8.",
+		"Opera/8.",
+		"Opera/9.",
+		"TwitterFeed 3",
+		"NAVER Blog Rssbot",
+		"AOL 9.0",
+		"rssreader@newstin.com",
+		"PHPCrawl",
+		"MSIE 2.",
+		"MSIE 4.",
+		"MSIE 5.",
+		"MSIE 6.",
+		"MSIE 7.0",
+		"Mozilla/0.",
+		"Mozilla/2.0",
+		"Mozilla/3.0",
+		"Mozilla/4.6",
+		"Mozilla/4.7",
+		"RSSIncludeBot/1.0", // cause exceptions in xml feeds
+		"Powermarks",
+		"GenwiFeeder",
+		"Akregator",
+		"ia_archiver",
+		"Atomic_Email_Hunter",
+		"Yahoo! Slurp",
+		"Python-urllib",
+		"BlackBerry",
+		"SimplePie", // Feeds parser
+		"www.webintegration.at", // crazy bot
+		"www.run4dom.com", // crazy bot
+		"zia-httpmirror",
+		"POE-Component-Client-HTTP",
+		"anonymous",
+		"Sosospider",
+		"Java/1.6",
+		"Shareaza",
+		"Jakarta Commons-HttpClient",
+		"Apache-HttpClient",
+		"Baiduspider",
+		"bingbot",
+		"MLBot", // www.metadatalabs.com/mlbot
+		"www.vbseo.com",
+		"yacybot", // yacy.net/bot.html
+		"SearchBot"
+	};
+
+	private static boolean isBot(String agent) {
+		if (agent == null)
+			return false;
+		for (String bot : bots) {
+			if (agent.indexOf(bot) >= 0)
+				return true;
+		}
+		return false;
+	}
+
+	private static final String[] bots = new String[]{
+		"Googlebot"
+	};
+
+	private HttpServlet getServlet(String path)
+		throws ServletException, ClassNotFoundException, IOException
+	{
+		int i = path.lastIndexOf('.');
+		if( i == -1 )
+			throw new ClassNotFoundException(path);
+		return getServletFromClass(
+			base + path.substring(0,i).replace('/','.')
+		);
+	}
+
+	private HttpServlet getServletFromClass(String cls)
+		throws ClassNotFoundException
+	{
+		synchronized(lock) {
+			if( reload && hasChanged(cls) ) {
+				unloadServlets();
+			}
+			HttpServlet srv = map.get(cls);
+			if( srv==null ) {
+				try {
+					Class clas = reload ? cl.loadClass(cls) : Class.forName(cls);
+					srv = (HttpServlet)clas.newInstance();
+				} catch(IllegalAccessException e) {
+					throw new RuntimeException(e);
+				} catch(InstantiationException e) {
+					throw new RuntimeException(e);
+				}
+				try {
+					srv.init(this);
+				} catch(ServletException e) {
+					throw new RuntimeException(e);
+				}
+				map.put(cls,srv);
+			}
+			return srv;
+		}
+	}
+
+	private boolean hasChanged(String cls) {
+		try {
+			URL url = cl.getResource( SimpleClassLoader.classToResource(cls) );
+			if( url==null )
+				return true;
+			File file = new File(url.getPath());
+			if( recompile ) {
+				String path = file.toString();
+				if( !path.endsWith(".class") )
+					throw new RuntimeException();
+				File dir = file.getParentFile();
+				String base = path.substring(0,path.length()-6);
+				File source = new File( base + ".jtp" );
+				if( source.lastModified() > clTime ) {
+					Process proc = Runtime.getRuntime().exec(new String[]{
+						"java", "fschmidt.tools.Jtp", source.getName()
+					},null,dir);
+					ProcUtils.checkProc(proc);
+				}
+				source = new File( base + ".java" );
+				if( source.lastModified() > clTime ) {
+					Process proc = Runtime.getRuntime().exec(new String[]{
+						"javac", "-g", source.getName()
+					},null,dir);
+					ProcUtils.checkProc(proc);
+				}
+			}
+			return file.lastModified() > clTime;
+		} catch(IOException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+
+	private long timeLimit = 0;
+	private static final String timeLimitAttr = "time-limit";
+
+	private static class TimeLimit {
+		long timeLimit;
+		final long startTime = System.currentTimeMillis();
+		long ioTime = 0L;
+
+		TimeLimit(long timeLimit) {
+			this.timeLimit = timeLimit;
+		}
+	}
+
+	public long getTimeLimit() {
+		return timeLimit;
+	}
+
+	public void setTimeLimit(long timeLimit) {
+		this.timeLimit = timeLimit;
+	}
+
+	private TimeLimit startTimeLimit(HttpServletRequest request) {
+		TimeLimit tl = new TimeLimit(timeLimit);
+		request.setAttribute( timeLimitAttr, tl );
+		return tl;
+	}
+
+	public void setTimeLimit(HttpServletRequest request,long timeLimit) {
+		TimeLimit tl = (TimeLimit)request.getAttribute(timeLimitAttr);
+		tl.timeLimit = timeLimit;
+	}
+
+	private void checkTimeLimit(HttpServletRequest request) {
+		TimeLimit tl = (TimeLimit)request.getAttribute(timeLimitAttr);
+		if( tl.timeLimit == 0L )
+			return;
+        long time = System.currentTimeMillis() - tl.startTime - tl.ioTime;
+        if( time > tl.timeLimit ) {
+			float free = Runtime.getRuntime().freeMemory();
+			float total = Runtime.getRuntime().totalMemory();
+			float used = (total - free) * 100f;
+			logger.error(ServletUtils.getCurrentURL(request,100) + " took " + time + " ms | " + String.format("%.1f",used/total) + '%');
+/*
+			Scheduler scheduler = TheScheduler.get();
+			if( scheduler instanceof ProfilingScheduler ) {
+				ProfilingScheduler profilingScheduler = (ProfilingScheduler)scheduler;
+				if( profilingScheduler.getMode()==ProfilingScheduler.Mode.FOREGROUND ) {
+					profilingScheduler.captureCPUSnapshot();
+				}
+			}
+*/
+		}
+	}
+
+	public static String getClientIpAddr(HttpServletRequest request) {
+		String ip = request.getHeader("X-Real-IP");
+		if( ip == null )
+			ip = request.getRemoteAddr();
+		return ip;
+	}
+
+}