My fork of airsonic with experimental fixes and improvements. See branch "custom"
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

338 lines
12 KiB

package org.libresonic.player.booter.deployer;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.net.BindException;
import java.util.Date;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import org.apache.commons.io.IOUtils;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.nio.SelectChannelConnector;
import org.mortbay.jetty.security.Constraint;
import org.mortbay.jetty.security.ConstraintMapping;
import org.mortbay.jetty.security.SslSocketConnector;
import org.mortbay.jetty.webapp.WebAppContext;
/**
* Responsible for deploying the Libresonic web app in
* the embedded Jetty container.
* <p/>
* The following system properties may be used to customize the behaviour:
* <ul>
* <li><code>libresonic.contextPath</code> - The context path at which Libresonic is deployed. Default "/".</li>
* <li><code>libresonic.port</code> - The port Libresonic will listen to. Default 4040.</li>
* <li><code>libresonic.httpsPort</code> - The port Libresonic will listen to for HTTPS. Default 0, which disables HTTPS.</li>
* <li><code>libresonic.war</code> - Libresonic WAR file, or exploded directory. Default "libresonic.war".</li>
* <li><code>libresonic.createLinkFile</code> - If set to "true", a Libresonic.url file is created in the working directory.</li>
* <li><code>libresonic.ssl.keystore</code> - Path to an alternate SSL keystore.</li>
* <li><code>libresonic.ssl.password</code> - Password of the alternate SSL keystore.</li>
* </ul>
*
* @author Sindre Mehus
*/
public class LibresonicDeployer implements LibresonicDeployerService {
public static final String DEFAULT_HOST = "0.0.0.0";
public static final int DEFAULT_PORT = 4040;
public static final int DEFAULT_HTTPS_PORT = 0;
public static final int DEFAULT_MEMORY_LIMIT = 150;
public static final String DEFAULT_CONTEXT_PATH = "/";
public static final String DEFAULT_WAR = "libresonic.war";
private static final int MAX_IDLE_TIME_MILLIS = 7 * 24 * 60 * 60 * 1000; // One week.
private static final int HEADER_BUFFER_SIZE = 64 * 1024;
// Libresonic home directory.
private static final File SUBSONIC_HOME_WINDOWS = new File("c:/libresonic");
private static final File SUBSONIC_HOME_OTHER = new File("/var/libresonic");
private Throwable exception;
private File libresonicHome;
private final Date startTime;
public LibresonicDeployer() {
// Enable shutdown hook for Ehcache.
System.setProperty("net.sf.ehcache.enableShutdownHook", "true");
startTime = new Date();
createLinkFile();
deployWebApp();
}
private void createLinkFile() {
if ("true".equals(System.getProperty("libresonic.createLinkFile"))) {
Writer writer = null;
try {
writer = new FileWriter("libresonic.url");
writer.append("[InternetShortcut]");
writer.append(System.getProperty("line.separator"));
writer.append("URL=").append(getUrl());
writer.flush();
} catch (Throwable x) {
System.err.println("Failed to create libresonic.url.");
x.printStackTrace();
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException x) {
// Ignored
}
}
}
}
}
private void deployWebApp() {
try {
Server server = new Server();
SelectChannelConnector connector = new SelectChannelConnector();
connector.setMaxIdleTime(MAX_IDLE_TIME_MILLIS);
connector.setHeaderBufferSize(HEADER_BUFFER_SIZE);
connector.setHost(getHost());
connector.setPort(getPort());
if (isHttpsEnabled()) {
connector.setConfidentialPort(getHttpsPort());
}
server.addConnector(connector);
if (isHttpsEnabled()) {
SslSocketConnector sslConnector = new SslSocketConnector();
sslConnector.setMaxIdleTime(MAX_IDLE_TIME_MILLIS);
sslConnector.setHeaderBufferSize(HEADER_BUFFER_SIZE);
sslConnector.setHost(getHost());
sslConnector.setPort(getHttpsPort());
sslConnector.setKeystore(System.getProperty("libresonic.ssl.keystore", getClass().getResource("/libresonic.keystore").toExternalForm()));
sslConnector.setPassword(System.getProperty("libresonic.ssl.password", "libresonic"));
server.addConnector(sslConnector);
}
WebAppContext context = new WebAppContext();
context.setTempDirectory(getJettyDirectory());
context.setContextPath(getContextPath());
context.setWar(getWar());
context.setOverrideDescriptor("/web-jetty.xml");
if (isHttpsEnabled()) {
// Allow non-https for streaming and cover art (for Chromecast, UPnP, Sonos etc)
context.getSecurityHandler().setConstraintMappings(new ConstraintMapping[]{
createConstraintMapping("/stream", Constraint.DC_NONE),
createConstraintMapping("/coverArt.view", Constraint.DC_NONE),
createConstraintMapping("/ws/*", Constraint.DC_NONE),
createConstraintMapping("/sonos/*", Constraint.DC_NONE),
createConstraintMapping("/", Constraint.DC_CONFIDENTIAL)
});
}
server.addHandler(context);
server.start();
System.err.println("Libresonic running on: " + getUrl());
if (isHttpsEnabled()) {
System.err.println(" and: " + getHttpsUrl());
}
} catch (Throwable x) {
x.printStackTrace();
exception = x;
}
}
private ConstraintMapping createConstraintMapping(String pathSpec, int dataConstraint) {
ConstraintMapping constraintMapping = new ConstraintMapping();
Constraint constraint = new Constraint();
constraint.setDataConstraint(dataConstraint);
constraintMapping.setPathSpec(pathSpec);
constraintMapping.setConstraint(constraint);
return constraintMapping;
}
private File getJettyDirectory() {
File dir = new File(getLibresonicHome(), "jetty");
String buildNumber = getLibresonicBuildNumber();
if (buildNumber != null) {
dir = new File(dir, buildNumber);
}
System.err.println("Extracting webapp to " + dir);
if (!dir.exists() && !dir.mkdirs()) {
System.err.println("Failed to create directory " + dir);
}
return dir;
}
private String getLibresonicBuildNumber() {
File war = new File(getWar());
InputStream in = null;
try {
if (war.isFile()) {
JarFile jar = new JarFile(war);
ZipEntry entry = jar.getEntry("WEB-INF\\classes\\build_number.txt");
if (entry == null) {
entry = jar.getEntry("WEB-INF/classes/build_number.txt");
}
in = jar.getInputStream(entry);
} else {
in = new FileInputStream(war.getPath() + "/WEB-INF/classes/build_number.txt");
}
return IOUtils.toString(in);
} catch (Exception x) {
System.err.println("Failed to resolve build number from WAR " + war + ": " + x);
return null;
} finally {
IOUtils.closeQuietly(in);
}
}
private String getContextPath() {
return System.getProperty("libresonic.contextPath", DEFAULT_CONTEXT_PATH);
}
private String getWar() {
String war = System.getProperty("libresonic.war");
if (war == null) {
war = DEFAULT_WAR;
}
File file = new File(war);
if (file.exists()) {
System.err.println("Using WAR file: " + file.getAbsolutePath());
} else {
System.err.println("Error: WAR file not found: " + file.getAbsolutePath());
}
return war;
}
private String getHost() {
return System.getProperty("libresonic.host", DEFAULT_HOST);
}
private int getPort() {
int port = DEFAULT_PORT;
String portString = System.getProperty("libresonic.port");
if (portString != null) {
port = Integer.parseInt(portString);
}
// Also set it so that the webapp can read it.
System.setProperty("libresonic.port", String.valueOf(port));
return port;
}
private int getHttpsPort() {
int port = DEFAULT_HTTPS_PORT;
String portString = System.getProperty("libresonic.httpsPort");
if (portString != null) {
port = Integer.parseInt(portString);
}
// Also set it so that the webapp can read it.
System.setProperty("libresonic.httpsPort", String.valueOf(port));
return port;
}
private boolean isHttpsEnabled() {
return getHttpsPort() > 0;
}
public String getErrorMessage() {
if (exception == null) {
return null;
}
if (exception instanceof BindException) {
return "Address already in use. Please change port number.";
}
return exception.toString();
}
public int getMemoryUsed() {
long freeBytes = Runtime.getRuntime().freeMemory();
long totalBytes = Runtime.getRuntime().totalMemory();
long usedBytes = totalBytes - freeBytes;
return (int) Math.round(usedBytes / 1024.0 / 1024.0);
}
private String getUrl() {
String host = DEFAULT_HOST.equals(getHost()) ? "localhost" : getHost();
StringBuilder url = new StringBuilder("http://").append(host);
if (getPort() != 80) {
url.append(":").append(getPort());
}
url.append(getContextPath());
return url.toString();
}
private String getHttpsUrl() {
if (!isHttpsEnabled()) {
return null;
}
String host = DEFAULT_HOST.equals(getHost()) ? "localhost" : getHost();
StringBuilder url = new StringBuilder("https://").append(host);
if (getHttpsPort() != 443) {
url.append(":").append(getHttpsPort());
}
url.append(getContextPath());
return url.toString();
}
/**
* Returns the Libresonic home directory.
*
* @return The Libresonic home directory, if it exists.
* @throws RuntimeException If directory doesn't exist.
*/
private File getLibresonicHome() {
if (libresonicHome != null) {
return libresonicHome;
}
File home;
String overrideHome = System.getProperty("libresonic.home");
if (overrideHome != null) {
home = new File(overrideHome);
} else {
boolean isWindows = System.getProperty("os.name", "Windows").toLowerCase().startsWith("windows");
home = isWindows ? SUBSONIC_HOME_WINDOWS : SUBSONIC_HOME_OTHER;
}
// Attempt to create home directory if it doesn't exist.
if (!home.exists() || !home.isDirectory()) {
boolean success = home.mkdirs();
if (success) {
libresonicHome = home;
} else {
String message = "The directory " + home + " does not exist. Please create it and make it writable. " +
"(You can override the directory location by specifying -Dlibresonic.home=... when " +
"starting the servlet container.)";
System.err.println("ERROR: " + message);
}
} else {
libresonicHome = home;
}
return home;
}
public DeploymentStatus getDeploymentInfo() {
return new DeploymentStatus(startTime, getUrl(), getHttpsUrl(), getMemoryUsed(), getErrorMessage());
}
}