/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see .
Copyright 2009 (C) Sindre Mehus
*/
package net.sourceforge.subsonic.service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.util.EntityUtils;
import net.sourceforge.subsonic.Logger;
import net.sourceforge.subsonic.domain.NATPMPRouter;
import net.sourceforge.subsonic.domain.Router;
import net.sourceforge.subsonic.domain.SBBIRouter;
import net.sourceforge.subsonic.domain.WeUPnPRouter;
import net.sourceforge.subsonic.util.StringUtil;
import net.sourceforge.subsonic.util.Util;
/**
* Provides network-related services, including port forwarding on UPnP routers and
* URL redirection from http://xxxx.subsonic.org.
*
* @author Sindre Mehus
*/
public class NetworkService {
private static final Logger LOG = Logger.getLogger(NetworkService.class);
private static final long PORT_FORWARDING_DELAY = 3600L;
private static final long URL_REDIRECTION_DELAY = 2 * 3600L;
private static final String URL_REDIRECTION_REGISTER_URL = getBackendUrl() + "/backend/redirect/register.view";
private static final String URL_REDIRECTION_UNREGISTER_URL = getBackendUrl() + "/backend/redirect/unregister.view";
private static final String URL_REDIRECTION_TEST_URL = getBackendUrl() + "/backend/redirect/test.view";
private SettingsService settingsService;
private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(4);
private final PortForwardingTask portForwardingTask = new PortForwardingTask();
private final URLRedirectionTask urlRedirectionTask = new URLRedirectionTask();
private Future> portForwardingFuture;
private Future> urlRedirectionFuture;
private final Status portForwardingStatus = new Status();
private final Status urlRedirectionStatus = new Status();
private boolean testUrlRedirection;
public void init() {
initPortForwarding();
initUrlRedirection(false);
}
/**
* Configures UPnP port forwarding.
*/
public synchronized void initPortForwarding() {
portForwardingStatus.setText("Idle");
if (portForwardingFuture != null) {
portForwardingFuture.cancel(true);
}
portForwardingFuture = executor.scheduleWithFixedDelay(portForwardingTask, 0L, PORT_FORWARDING_DELAY, TimeUnit.SECONDS);
}
/**
* Configures URL redirection.
*
* @param test Whether to test that the redirection works.
*/
public synchronized void initUrlRedirection(boolean test) {
urlRedirectionStatus.setText("Idle");
if (urlRedirectionFuture != null) {
urlRedirectionFuture.cancel(true);
}
testUrlRedirection = test;
urlRedirectionFuture = executor.scheduleWithFixedDelay(urlRedirectionTask, 0L, URL_REDIRECTION_DELAY, TimeUnit.SECONDS);
}
public Status getPortForwardingStatus() {
return portForwardingStatus;
}
public Status getURLRedirecionStatus() {
return urlRedirectionStatus;
}
public static String getBackendUrl() {
return "true".equals(System.getProperty("subsonic.test")) ? "http://localhost:8181" : "http://subsonic.org";
}
public void setSettingsService(SettingsService settingsService) {
this.settingsService = settingsService;
}
private class PortForwardingTask extends Task {
@Override
protected void execute() {
boolean enabled = settingsService.isPortForwardingEnabled();
portForwardingStatus.setText("Looking for router...");
Router router = findRouter();
if (router == null) {
LOG.warn("No UPnP router found.");
portForwardingStatus.setText("No router found.");
} else {
portForwardingStatus.setText("Router found.");
int port = settingsService.getPort();
int httpsPort = settingsService.getHttpsPort();
// Create new NAT entry.
if (enabled) {
try {
router.addPortMapping(port, port, 0);
String message = "Successfully forwarding port " + port;
if (httpsPort != 0 && httpsPort != port) {
router.addPortMapping(httpsPort, httpsPort, 0);
message += " and port " + httpsPort;
}
message += ".";
LOG.info(message);
portForwardingStatus.setText(message);
} catch (Throwable x) {
String message = "Failed to create port forwarding.";
LOG.warn(message, x);
portForwardingStatus.setText(message + " See log for details.");
}
}
// Delete NAT entry.
else {
try {
router.deletePortMapping(port, port);
LOG.info("Deleted port mapping for port " + port);
if (httpsPort != 0 && httpsPort != port) {
router.deletePortMapping(httpsPort, httpsPort);
LOG.info("Deleted port mapping for port " + httpsPort);
}
} catch (Throwable x) {
LOG.warn("Failed to delete port mapping.", x);
}
portForwardingStatus.setText("Port forwarding disabled.");
}
}
// Don't do it again if disabled.
if (!enabled && portForwardingFuture != null) {
portForwardingFuture.cancel(false);
}
}
private Router findRouter() {
try {
Router router = SBBIRouter.findRouter();
if (router != null) {
return router;
}
} catch (Throwable x) {
LOG.warn("Failed to find UPnP router using SBBI library.", x);
}
try {
Router router = WeUPnPRouter.findRouter();
if (router != null) {
return router;
}
} catch (Throwable x) {
LOG.warn("Failed to find UPnP router using WeUPnP library.", x);
}
try {
Router router = NATPMPRouter.findRouter();
if (router != null) {
return router;
}
} catch (Throwable x) {
LOG.warn("Failed to find NAT-PMP router.", x);
}
return null;
}
}
private class URLRedirectionTask extends Task {
@Override
protected void execute() {
boolean enable = settingsService.isUrlRedirectionEnabled();
HttpPost request = new HttpPost(enable ? URL_REDIRECTION_REGISTER_URL : URL_REDIRECTION_UNREGISTER_URL);
int port = settingsService.getPort();
boolean trial = !settingsService.isLicenseValid();
Date trialExpires = settingsService.getUrlRedirectTrialExpires();
List params = new ArrayList();
params.add(new BasicNameValuePair("serverId", settingsService.getServerId()));
params.add(new BasicNameValuePair("redirectFrom", settingsService.getUrlRedirectFrom()));
params.add(new BasicNameValuePair("port", String.valueOf(port)));
params.add(new BasicNameValuePair("localIp", Util.getLocalIpAddress()));
params.add(new BasicNameValuePair("localPort", String.valueOf(port)));
params.add(new BasicNameValuePair("contextPath", settingsService.getUrlRedirectContextPath()));
params.add(new BasicNameValuePair("trial", String.valueOf(trial)));
if (trial && trialExpires != null) {
params.add(new BasicNameValuePair("trialExpires", String.valueOf(trialExpires.getTime())));
} else {
params.add(new BasicNameValuePair("licenseHolder", settingsService.getLicenseEmail()));
}
HttpClient client = new DefaultHttpClient();
try {
urlRedirectionStatus.setText(enable ? "Registering web address..." : "Unregistering web address...");
request.setEntity(new UrlEncodedFormEntity(params, StringUtil.ENCODING_UTF8));
HttpResponse response = client.execute(request);
StatusLine status = response.getStatusLine();
switch (status.getStatusCode()) {
case HttpStatus.SC_BAD_REQUEST:
urlRedirectionStatus.setText(EntityUtils.toString(response.getEntity()));
break;
case HttpStatus.SC_OK:
urlRedirectionStatus.setText(enable ? "Successfully registered web address." : "Web address disabled.");
break;
default:
throw new IOException(status.getStatusCode() + " " + status.getReasonPhrase());
}
} catch (Throwable x) {
LOG.warn(enable ? "Failed to register web address." : "Failed to unregister web address.", x);
urlRedirectionStatus.setText(enable ? ("Failed to register web address. " + x.getMessage() +
" (" + x.getClass().getSimpleName() + ")") : "Web address disabled.");
} finally {
client.getConnectionManager().shutdown();
}
// Test redirection, but only once.
if (testUrlRedirection) {
testUrlRedirection = false;
testUrlRedirection();
}
// Don't do it again if disabled.
if (!enable && urlRedirectionFuture != null) {
urlRedirectionFuture.cancel(false);
}
}
private void testUrlRedirection() {
HttpGet request = new HttpGet(URL_REDIRECTION_TEST_URL + "?redirectFrom=" + settingsService.getUrlRedirectFrom());
HttpClient client = new DefaultHttpClient();
HttpConnectionParams.setConnectionTimeout(client.getParams(), 10000);
HttpConnectionParams.setSoTimeout(client.getParams(), 30000);
try {
urlRedirectionStatus.setText("Testing web address " + settingsService.getUrlRedirectFrom() + ".subsonic.org. Please wait...");
String response = client.execute(request, new BasicResponseHandler());
urlRedirectionStatus.setText(response);
} catch (Throwable x) {
LOG.warn("Failed to test web address.", x);
urlRedirectionStatus.setText("Failed to test web address. " + x.getMessage() + " (" + x.getClass().getSimpleName() + ")");
} finally {
client.getConnectionManager().shutdown();
}
}
}
private abstract class Task implements Runnable {
public void run() {
String name = getClass().getSimpleName();
try {
execute();
} catch (Throwable x) {
LOG.error("Error executing " + name + ": " + x.getMessage(), x);
}
}
protected abstract void execute();
}
public static class Status {
private String text;
private Date date;
public void setText(String text) {
this.text = text;
date = new Date();
}
public String getText() {
return text;
}
public Date getDate() {
return date;
}
}
}