/** * FileHandler * Copyright 10.02.2016 by Michael Peter Christen, @0rb1t3r * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program in the file lgpl21.txt * If not, see <http://www.gnu.org/licenses/>. */ package org.loklak.server; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.nio.file.Files; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.handler.ResourceHandler; import org.eclipse.jetty.util.resource.PathResource; import org.eclipse.jetty.util.resource.Resource; import org.loklak.tools.ByteBuffer; import org.loklak.tools.UTF8; public class FileHandler extends ResourceHandler implements Handler { private final long CACHE_LIMIT = 128L * 1024L; private int expiresSeconds = 0; /** * cerate a custom ResourceHandler with more caching * @param expiresSeconds the time each file shall stay in the cache */ public FileHandler(int expiresSeconds) { this.expiresSeconds = expiresSeconds; //this.setMinMemoryMappedContentLength((int) CACHE_LIMIT); } @Override public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { // use the ResourceHandler to handle the request. This method calls doResponseHeaders internally super.handle(target, baseRequest, request, response); } @Override protected void doResponseHeaders(HttpServletResponse response, Resource resource, String mimeType) { if (mimeType == null && resource.getName().endsWith(".css")) mimeType = "text/css"; super.doResponseHeaders(response, resource, mimeType); // modify the caching strategy of ResourceHandler setCaching(response, this.expiresSeconds); } public static void setCaching(final HttpServletResponse response, final int expiresSeconds) { if (response instanceof org.eclipse.jetty.server.Response) { org.eclipse.jetty.server.Response r = (org.eclipse.jetty.server.Response) response; HttpFields fields = r.getHttpFields(); // remove the last-modified field since caching otherwise does not work /* https://www.ietf.org/rfc/rfc2616.txt "if the response does have a Last-Modified time, the heuristic expiration value SHOULD be no more than some fraction of the interval since that time. A typical setting of this fraction might be 10%." */ fields.remove(HttpHeader.LAST_MODIFIED); // if this field is present, the reload-time is a 10% fraction of ttl and other caching headers do not work // cache-control: allow shared caching (i.e. proxies) and set expires age for cache if(expiresSeconds == 0){ fields.put(HttpHeader.CACHE_CONTROL, "public, no-store, max-age=" + Integer.toString(expiresSeconds)); // seconds } else { fields.put(HttpHeader.CACHE_CONTROL, "public, max-age=" + Integer.toString(expiresSeconds)); // seconds } } else { response.setHeader(HttpHeader.LAST_MODIFIED.asString(), ""); // not really the best wqy to remove this header but there is no other option if(expiresSeconds == 0){ response.setHeader(HttpHeader.CACHE_CONTROL.asString(), "public, no-store, max-age=" + Integer.toString(expiresSeconds)); } else{ response.setHeader(HttpHeader.CACHE_CONTROL.asString(), "public, max-age=" + Integer.toString(expiresSeconds)); } } // expires: define how long the file shall stay in a cache if cache-control is not used for this information response.setDateHeader(HttpHeader.EXPIRES.asString(), System.currentTimeMillis() + expiresSeconds * 1000); } @Override public Resource getResource(String path) { Resource resource = super.getResource(path); if (resource == null) return null; if (!(resource instanceof PathResource) || !resource.exists()) return resource; try { File f = resource.getFile(); if (f.isDirectory() && !path.equals("/")) return resource; CacheResource cache = resourceCache.get(f); if (cache != null) return cache; if (f.length() < CACHE_LIMIT || f.getName().endsWith(".html") || path.equals("/")) { cache = new CacheResource((PathResource) resource); resourceCache.put(f, cache); return cache; } } catch (IOException e) { e.printStackTrace(); } return resource; } private final Map<File, CacheResource> resourceCache = new ConcurrentHashMap<>(); private final static byte[] SSI_START = "<!--#include file=\"".getBytes(); private final static byte[] SSI_END = "\" -->".getBytes(); private static class CacheResource extends Resource { private byte[] buffer; private long lastModified; private File file; private List<File> includes; public CacheResource(PathResource pathResource) throws IOException { this.file = pathResource.getFile(); if (this.file.isDirectory()) this.file = new File(this.file, "index.html"); this.includes = new ArrayList<>(8); initCache(System.currentTimeMillis()); pathResource.close(); } private void initCache(long nextLastModified) throws IOException { this.buffer = Files.readAllBytes(this.file.toPath()); if (this.file.getName().endsWith(".html")) this.buffer = insertSSI(this.buffer); this.lastModified = nextLastModified; } private byte[] insertSSI(byte[] b) throws IOException { this.includes.clear(); for (int p = findSSI_start(b, 0); p >= 0; p = findSSI_start(b, p)) { int q = findSSI_end(b, p); if (q < 0) break; byte[] f = new byte[q - p - SSI_START.length]; System.arraycopy(b, p + SSI_START.length, f, 0, f.length); File ff = new File(this.file.getParent(), UTF8.String(f)).getCanonicalFile(); if (!ff.exists()) { byte[] b0 = new byte[b.length - (q - p) - SSI_END.length]; System.arraycopy(b, 0, b0, 0, p); System.arraycopy(b, q + SSI_END.length, b0, p, b.length - q - SSI_END.length); b = b0; continue; } this.includes.add(ff); byte[] i = Files.readAllBytes(ff.toPath()); byte[] b0 = new byte[b.length - (q - p) - SSI_END.length + i.length]; System.arraycopy(b, 0, b0, 0, p); System.arraycopy(i, 0, b0, p, i.length); System.arraycopy(b, q + SSI_END.length, b0, p + i.length, b.length - q - SSI_END.length); b = b0; } return b; } private int findSSI_start(byte[] b, int p) { return ByteBuffer.indexOf(b, SSI_START, p); } private int findSSI_end(byte[] b, int p) { return ByteBuffer.indexOf(b, SSI_END, p); } @Override public boolean exists() { return true; } @Override public boolean isDirectory() { return false; } private long actualLastModified() { long l = this.file.lastModified(); for (File d: this.includes) l = Math.max(l, d.lastModified()); return l; } @Override public long lastModified() { long l = actualLastModified(); if (actualLastModified() > this.lastModified) try {initCache(l);} catch (IOException e) {} this.lastModified = l; return this.lastModified; } @Override public long length() { return this.buffer.length; } @Override public InputStream getInputStream() throws IOException { long l = actualLastModified(); if (actualLastModified() > this.lastModified) initCache(l); return new ByteArrayInputStream(this.buffer); } @Override public ReadableByteChannel getReadableByteChannel() throws IOException { return Channels.newChannel(this.getInputStream()); } @Override public String getName() { return this.file.getName(); } @Override public void close() {} @Override public String[] list() {throw new UnsupportedOperationException();} @Override public boolean delete() throws SecurityException {throw new UnsupportedOperationException();} @Override public Resource addPath(String arg0) throws IOException, MalformedURLException {throw new UnsupportedOperationException();} @Override public File getFile() throws IOException {throw new UnsupportedOperationException();} @Override public URL getURL() {throw new UnsupportedOperationException();} @Override public boolean isContainedIn(Resource arg0) throws MalformedURLException {throw new UnsupportedOperationException();} @Override public boolean renameTo(Resource arg0) throws SecurityException {throw new UnsupportedOperationException();} } }