package act.handler.builtin; /*- * #%L * ACT Framework * %% * Copyright (C) 2014 - 2017 ActFramework * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ import act.Act; import act.app.ActionContext; import act.app.App; import act.controller.ParamNames; import act.handler.builtin.controller.FastRequestHandler; import org.osgl.$; import org.osgl.http.H; import org.osgl.mvc.result.NotFound; import org.osgl.util.E; import org.osgl.util.IO; import org.osgl.util.S; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.ByteBuffer; import java.util.*; import static org.osgl.http.H.Format.*; /** * Unlike a {@link act.handler.builtin.StaticFileGetter}, the * `StaticResourceGetter` read resource from jar packages */ public class StaticResourceGetter extends FastRequestHandler { private static final char SEP = '/'; private String base; private URL baseUrl; private int preloadSizeLimit; private boolean isFolder; private ByteBuffer buffer; private H.Format contentType; private boolean preloadFailure; private boolean preloaded; private String etag; private Set<URL> folders = new HashSet<>(); private Map<String, String> etags = new HashMap<>(); private Map<String, ByteBuffer> cachedBuffers = new HashMap<>(); private Map<String, String> cachedContentType = new HashMap<>(); private Map<String, Boolean> cachedFailures = new HashMap<>(); public StaticResourceGetter(String base) { String path = S.ensureStartsWith(base, SEP); this.base = path; this.baseUrl = StaticFileGetter.class.getResource(path); E.illegalArgumentIf(null == this.baseUrl, "Cannot find base URL: %s", base); this.isFolder = isFolder(this.baseUrl, path); if (!this.isFolder && "file".equals(baseUrl.getProtocol())) { Act.jobManager().beforeAppStart(new Runnable() { @Override public void run() { preloadCache(); } }); } this.preloadSizeLimit = Act.appConfig().resourcePreloadSizeLimit(); } @Override protected void releaseResources() { } @Override public boolean express(ActionContext context) { if (preloaded) { return true; } String path = context.paramVal(ParamNames.PATH); return Act.isProd() && (cachedBuffers.containsKey(path) || cachedFailures.containsKey(path) || (null != context.req().etag() && context.req().etagMatches(etags.get(path)))); } @Override public void handle(ActionContext context) { context.handler(this); String path = context.paramVal(ParamNames.PATH); handle(path, context); } protected void handle(String path, ActionContext context) { H.Request req = context.req(); if (Act.isProd()) { if (preloaded) { // this is a reloaded file resource if (preloadFailure) { AlwaysNotFound.INSTANCE.handle(context); } else { if (req.etagMatches(etag)) { AlwaysNotModified.INSTANCE.handle(context); } else { H.Response resp = context.resp(); resp.contentType(contentType.contentType()) .etag(this.etag) .writeContent(buffer.duplicate()); } } return; } if (cachedFailures.containsKey(path)) { AlwaysNotFound.INSTANCE.handle(context); return; } if (null != req.etag() && req.etagMatches(etags.get(path))) { AlwaysNotModified.INSTANCE.handle(context); return; } } ByteBuffer buffer = cachedBuffers.get(path); if (null != buffer) { context.resp().contentType(cachedContentType.get(path)) .etag(etags.get(path)) .writeContent(buffer.duplicate()); return; } try { URL target; H.Format fmt; String loadPath; if (S.blank(path)) { target = baseUrl; loadPath = base; } else { loadPath = S.pathConcat(base, SEP, path); target = StaticFileGetter.class.getResource(loadPath); if (null == target) { throw NotFound.get(); } } if (preventFolderAccess(target, loadPath, context)) { return; } fmt = StaticFileGetter.contentType(target.getPath()); H.Response resp = context.resp(); resp.contentType(fmt.contentType()); context.applyCorsSpec().applyContentType(); try { int n = IO.copy(target.openStream(), resp.outputStream()); if (Act.isProd()) { etags.put(path, String.valueOf(n)); if (n < context.config().resourcePreloadSizeLimit()) { $.Var<String> etagBag = $.var(); buffer = doPreload(target, etagBag); if (null == buffer) { cachedFailures.put(path, true); } else { cachedBuffers.put(path, buffer); cachedContentType.put(path, fmt.contentType()); } } } } catch (NullPointerException e) { // this is caused by accessing folder inside jar URL folders.add(target); AlwaysForbidden.INSTANCE.handle(context); } } catch (IOException e) { App.logger.warn(e, "Error servicing static resource request"); throw NotFound.get(); } } private boolean preventFolderAccess(URL target, String path, ActionContext context) { if (folders.contains(target)) { AlwaysForbidden.INSTANCE.handle(context); return true; } if (isFolder(target, path)) { folders.add(target); AlwaysForbidden.INSTANCE.handle(context); return true; } return false; } private boolean isFolder(URL target, String path) { if ("file".equals(target.getProtocol())) { File file = new File(target.getFile()); return file.isDirectory(); } if ("jar".equals(target.getProtocol())) { if (path.endsWith("/")) { return true; } URL url = StaticFileGetter.class.getResource(S.ensureEndsWith(path, "/")); return null != url; } return false; } private void preloadCache() { if (Act.isDev()) { return; } contentType = StaticFileGetter.contentType(baseUrl.getPath()); if (HTML == contentType || CSS == contentType || JAVASCRIPT == contentType || TXT == contentType || CSV == contentType || JSON == contentType || XML == contentType || resourceSizeIsOkay()) { $.Var<String> etagBag = $.var(); buffer = doPreload(baseUrl, etagBag); if (null == buffer) { preloadFailure = true; } else { this.etag = etagBag.get(); } preloaded = true; } } private ByteBuffer doPreload(URL target, $.Var<String> etagBag) { try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); IO.copy(target.openStream(), baos); byte[] ba = baos.toByteArray(); buffer = ByteBuffer.wrap(ba); etagBag.set(String.valueOf(Arrays.hashCode(ba))); return buffer; } catch (IOException e) { Act.LOGGER.warn(e, "Error loading resource: %s", baseUrl.getPath()); } return null; } private boolean resourceSizeIsOkay() { if (preloadSizeLimit <= 0) { return false; } if ("file".equals(baseUrl.getProtocol())) { File file = new File(baseUrl.getFile()); return file.length() < preloadSizeLimit; } return false; } @Override public boolean supportPartialPath() { return isFolder; } @Override public String toString() { return baseUrl.toString(); } }