/* * Copyright 2013-2014 Odysseus Software GmbH * * 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. */ package org.musicmount.server; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.security.Principal; import java.util.Arrays; import java.util.logging.Level; import java.util.logging.Logger; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.catalina.Context; import org.apache.catalina.LifecycleState; import org.apache.catalina.Wrapper; import org.apache.catalina.authenticator.BasicAuthenticator; import org.apache.catalina.core.StandardContext; import org.apache.catalina.deploy.FilterDef; import org.apache.catalina.deploy.FilterMap; import org.apache.catalina.deploy.LoginConfig; import org.apache.catalina.deploy.SecurityCollection; import org.apache.catalina.deploy.SecurityConstraint; import org.apache.catalina.realm.GenericPrincipal; import org.apache.catalina.realm.RealmBase; import org.apache.catalina.servlets.DefaultServlet; import org.apache.catalina.startup.Tomcat; import org.musicmount.util.LoggingUtil; public class MusicMountServerTomcat implements MusicMountServer { static final Logger LOGGER = Logger.getLogger(MusicMountServerTomcat.class.getName()); static { System.setProperty("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE", "true"); LoggingUtil.configure("org.apache", Level.INFO); } final Filter AccessLogFilter = new Filter() { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { final long requestTimestamp = System.currentTimeMillis(); chain.doFilter(request, response); // serve request if (accessLog != null) { response.flushBuffer(); final HttpServletRequest httpRequest = (HttpServletRequest) request; final HttpServletResponse httpResponse = (HttpServletResponse) response; final long responseTimestamp = System.currentTimeMillis(); accessLog.log(new AccessLog.Entry() { @Override public long getResponseTimestamp() { return responseTimestamp; } @Override public int getResponseStatus() { return httpResponse.getStatus(); } @Override public String getResponseHeader(String header) { return httpResponse.getHeader(header); } @Override public String getRequestURI() { return httpRequest.getRequestURI(); } @Override public long getRequestTimestamp() { return requestTimestamp; } @Override public String getRequestMethod() { return httpRequest.getMethod(); } }); } } @Override public void destroy() { } }; static final Filter UTF8Filter = new Filter() { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { response.setCharacterEncoding("UTF-8"); chain.doFilter(request, response); } @Override public void destroy() { } }; private static Context addContext(Tomcat tomcat, String contextPath, File baseDir, String servletMapping) { Context context = tomcat.addContext(contextPath, baseDir.getAbsolutePath()); Wrapper defaultServlet = context.createWrapper(); defaultServlet.setName("default"); defaultServlet.setServlet(new DefaultServlet()); defaultServlet.addInitParameter("debug", "0"); defaultServlet.addInitParameter("listings", "false"); defaultServlet.setLoadOnStartup(1); context.addChild(defaultServlet); context.addServletMapping(servletMapping, "default"); return context; } private static Context addContext(Tomcat tomcat, String contextPath, File baseDir) { return addContext(tomcat, contextPath, baseDir, "/*"); } private static void addUTF8Filter(Context context) { FilterDef utf8FilterDef = new FilterDef(); utf8FilterDef.setFilterName("utf8-filter"); utf8FilterDef.setFilter(UTF8Filter); FilterMap utf8FilterMap = new FilterMap(); utf8FilterMap.setFilterName("utf8-filter"); utf8FilterMap.addURLPattern("*"); context.addFilterDef(utf8FilterDef); context.addFilterMap(utf8FilterMap); } private static void addBasicAuth(StandardContext context) { SecurityConstraint securityConstraint = new SecurityConstraint(); securityConstraint.addAuthRole("user"); SecurityCollection securityCollection = new SecurityCollection(); // securityCollection.addMethod("GET"); // defaults to all methods securityCollection.addPattern("/*"); securityConstraint.addCollection(securityCollection); LoginConfig loginConfig = new LoginConfig(); loginConfig.setAuthMethod("BASIC"); loginConfig.setRealmName("MusiMount"); context.addConstraint(securityConstraint); context.setLoginConfig(loginConfig); context.addValve(new BasicAuthenticator()); } private static RealmBase createRealm(final String user, final String password) { return new RealmBase() { @Override protected Principal getPrincipal(String username) { String password = getPassword(username); return password != null ? new GenericPrincipal(username, password, Arrays.asList("user")) : null; } @Override protected String getPassword(String username) { return user.equals(username) ? password : null; } @Override protected String getName() { return "MusicMount"; } }; } private static boolean deleteRecursive(File parent) { if (parent.isDirectory()) { for (File child : parent.listFiles()) { deleteRecursive(child); } } return parent.delete(); } private Tomcat tomcat; private Path workDir; private final AccessLog accessLog; public MusicMountServerTomcat(AccessLog accessLog) { this.accessLog = accessLog; } private Tomcat createTomcat(int port) throws IOException { workDir = Files.createTempDirectory("musicmount-"); Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { @Override public void run() { if (Files.exists(workDir) && !deleteRecursive(workDir.toFile())) { LOGGER.warning("Could not delete temporary directory: " + workDir); } } })); tomcat = new Tomcat(); tomcat.setBaseDir(workDir.toFile().getAbsolutePath()); tomcat.setPort(port); tomcat.getConnector().setURIEncoding("UTF-8"); tomcat.getConnector().setProperty("compression", "on"); tomcat.getConnector().setProperty("compressionMinSize", "1024"); tomcat.getConnector().setProperty("compressableMimeType", "text/json"); tomcat.setSilent(true); // TODO: this fixes a resource-loading problem in WebappClassLoader when running through com.javafx.main.Main tomcat.getEngine().setParentClassLoader(Thread.currentThread().getContextClassLoader()); return tomcat; } private void addLogFilter(Context... contexts) { FilterDef logFilterDef = new FilterDef(); logFilterDef.setFilterName("log-filter"); logFilterDef.setFilter(AccessLogFilter); FilterMap logFilterMap = new FilterMap(); logFilterMap.setFilterName("log-filter"); logFilterMap.addURLPattern("*"); for (Context context : contexts) { context.addFilterDef(logFilterDef); context.addFilterMap(logFilterMap); } } @Override public void start(FolderContext music, FolderContext mount, int port, final String user, final String password) throws Exception { Tomcat tomcat = createTomcat(port); Context mountContext = addContext(tomcat, mount.getPath(), mount.getFolder().getAbsoluteFile()); mountContext.addWelcomeFile("index.json"); mountContext.addMimeMapping("json", "text/json"); Context musicContext = addContext(tomcat, music.getPath(), music.getFolder().getAbsoluteFile()); musicContext.addMimeMapping("m4a", "audio/mp4"); musicContext.addMimeMapping("mp3", "audio/mpeg"); addUTF8Filter(mountContext); addLogFilter(mountContext, musicContext); if (user != null && password != null) { tomcat.getEngine().setRealm(createRealm(user, password)); addBasicAuth((StandardContext) mountContext); addBasicAuth((StandardContext) musicContext); } tomcat.start(); } @Override public void start(FolderContext music, MountContext mount, int port, String user, String password) throws Exception { Tomcat tomcat = createTomcat(port); Context mountContext = addContext(tomcat, mount.getPath(), workDir.toFile()); Wrapper mountServlet = mountContext.createWrapper(); mountServlet.setName("mount"); mountServlet.setServlet(mount.getServlet()); mountServlet.setLoadOnStartup(1); mountContext.addChild(mountServlet); mountContext.addServletMapping("/*", "mount"); Context musicContext = addContext(tomcat, music.getPath(), music.getFolder().getAbsoluteFile()); musicContext.addMimeMapping("m4a", "audio/mp4"); musicContext.addMimeMapping("mp3", "audio/mpeg"); addUTF8Filter(mountContext); addLogFilter(mountContext, musicContext); if (user != null && password != null) { tomcat.getEngine().setRealm(createRealm(user, password)); addBasicAuth((StandardContext) mountContext); addBasicAuth((StandardContext) musicContext); } tomcat.start(); } public void await() { tomcat.getServer().await(); } public boolean isStarted() { return tomcat != null && tomcat.getServer().getState() == LifecycleState.STARTED; } public void stop() throws Exception { tomcat.stop(); tomcat.destroy(); tomcat = null; if (workDir != null && !deleteRecursive(workDir.toFile())) { LOGGER.warning("Could not delete temporary directory: " + workDir); } workDir = null; } }