// UPnP.java
// (C) 2009 by David Wieditz; d.wieditz@gmx.de
// first published 14.02.2009 on http://yacy.net
//
// This is a part of YaCy, a peer-to-peer based web search engine
//
// LICENSE
//
// This program 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 2 of the License, or
// (at your option) any later version.
//
// This program 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 this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package net.yacy.utils.upnp;
import java.io.IOException;
import java.net.InetAddress;
import java.util.EnumMap;
import java.util.Map;
import java.util.Map.Entry;
import javax.xml.parsers.ParserConfigurationException;
import net.yacy.cora.util.ConcurrentLog;
import net.yacy.search.Switchboard;
import net.yacy.search.SwitchboardConstants;
import org.bitlet.weupnp.GatewayDevice;
import org.bitlet.weupnp.GatewayDiscover;
import org.bitlet.weupnp.PortMappingEntry;
import org.xml.sax.SAXException;
/**
* Maps port(s) in LAN to port(s) in WAN vie UPnP.
*
* @author David Wieditz, Marc Nause
*/
public class UPnP {
private static final ConcurrentLog LOG = new ConcurrentLog("UPNP");
private static final Switchboard SB = Switchboard.getSwitchboard();
private static GatewayDevice gatewayDevice;
private static final Map<UPnPMappingType, UPnPMapping> MAPPINGS = new EnumMap<>(
UPnPMappingType.class);
static {
MAPPINGS.put(UPnPMappingType.HTTP, new UPnPMapping(SwitchboardConstants.SERVER_PORT, null, "TCP",
"YaCy HTTP"));
MAPPINGS.put(UPnPMappingType.HTTPS, new UPnPMapping(SwitchboardConstants.SERVER_SSLPORT,
"server.https", "TCP", "YaCy HTTPS"));
}
private static final int MIN_CANDIDATE_PORT = 49152;
private static final int MAX_CANDIDATE_PORT = 65535;
private static boolean init() {
boolean init = true;
try {
if (gatewayDevice == null || !gatewayDevice.isConnected()) {
final GatewayDiscover discover = new GatewayDiscover();
discover.discover();
gatewayDevice = discover.getValidGateway();
}
} catch (IOException | SAXException | ParserConfigurationException e) {
init = false;
}
if (gatewayDevice != null) {
LOG.info("found device: " + gatewayDevice.getFriendlyName());
} else {
LOG.info("no device found");
init = false;
}
return init;
}
/**
* Add port mappings for configured ports.
*/
public static void addPortMappings() {
if (SB == null) {
return;
}
UPnPMapping mapping;
for (final Entry<UPnPMappingType, UPnPMapping> entry : MAPPINGS
.entrySet()) {
mapping = entry.getValue();
addPortMapping(entry.getKey(), mapping,
SB.getConfigInt(mapping.getConfigPortKey(), 0));
}
SB.setConnectedViaUpnp(true);
}
/**
* Remove all port mappings.
*/
public static void deletePortMappings() {
if (SB == null) {
return;
}
SB.setConnectedViaUpnp(false);
UPnPMapping mapping;
for (final Entry<UPnPMappingType, UPnPMapping> entry : MAPPINGS
.entrySet()) {
mapping = entry.getValue();
deletePortMapping(mapping);
}
}
/**
* Add port mapping to all gateway devices on the network.<br/>
* Latest port mapping will be removed.
*
* @param type
* mapping type
* @param mapping
* contains data about mapping
* @param port
* port number to map
*/
private static void addPortMapping(final UPnPMappingType type,
final UPnPMapping mapping, final int port) {
if (port < 1) {
return;
}
if (mapping.getPort() > 0) {
deletePortMapping(mapping); // delete old mapping first
}
if ((mapping.isConfigEnabledKeyEmpty() || SB.getConfigBool(
mapping.getConfigEnabledKey(), false))
&& mapping.getPort() == 0
&& ((gatewayDevice != null) || init())) {
String localHostIP;
boolean mapped;
String msg;
try {
localHostIP = toString(gatewayDevice.getLocalAddress());
int portCandidate = port;
while (isInUse(portCandidate) && portCandidate > 0) {
portCandidate = getNewPortCandidate(portCandidate);
}
if (portCandidate > 0) {
mapped = gatewayDevice.addPortMapping(portCandidate, port,
localHostIP, mapping.getProtocol(),
mapping.getDescription());
msg = "mapped port " + port + " to port " + portCandidate
+ " on device " + gatewayDevice.getFriendlyName()
+ ", external IP is "
+ gatewayDevice.getExternalIPAddress();
} else {
mapped = false;
msg = "no free port found";
}
if (mapped) {
LOG.info("mapped " + msg);
mapping.setPort(portCandidate);
SB.setUpnpPorts(mapping.getConfigPortKey(), portCandidate);
} else {
LOG.warn("could not map " + msg);
}
} catch (IOException | SAXException e) {
LOG.severe("mapping error: " + e.getMessage());
}
}
}
/**
* Delete current port mapping.
*
* @param mapping
* to delete
*/
private static void deletePortMapping(final UPnPMapping mapping) {
if (mapping.getPort() > 0 && gatewayDevice != null) {
boolean unmapped;
String msg;
try {
unmapped = gatewayDevice.deletePortMapping(mapping.getPort(),
mapping.getProtocol());
msg = "port " + mapping.getPort() + " on device "
+ gatewayDevice.getFriendlyName();
if (unmapped) {
LOG.info("unmapped " + msg);
} else {
LOG.warn("could not unmap " + msg);
}
} catch (SAXException | IOException e) {
LOG.severe("unmapping error: " + e.getMessage());
}
}
mapping.setPort(0); // reset mapped port
}
/**
* Gets currently mapped port.
*
* @param type
* mapping type
*
* @return mapped port or 0 if no port is mapped
*/
public static int getMappedPort(final UPnPMappingType type) {
if (type == null) {
return 0;
}
return MAPPINGS.get(type).getPort();
}
private static int getNewPortCandidate(final int oldCandidate) {
int newPortCandidate = Math.min(
Math.max(MIN_CANDIDATE_PORT, oldCandidate + 1),
MAX_CANDIDATE_PORT);
if (newPortCandidate == MAX_CANDIDATE_PORT) {
newPortCandidate = -1;
}
return newPortCandidate;
}
private static boolean isInUse(final int port) {
try {
return gatewayDevice != null
&& gatewayDevice.getSpecificPortMappingEntry(port, "TCP",
new PortMappingEntry());
} catch (IOException | SAXException e) {
return false;
}
}
private static String toString(final InetAddress inetAddress) {
final String localHostIP;
if (inetAddress != null) {
localHostIP = inetAddress.getHostAddress();
if (!inetAddress.isSiteLocalAddress()
|| localHostIP.startsWith("127.")) {
LOG.warn("found odd local address: " + localHostIP
+ "; UPnP may fail");
}
} else {
localHostIP = "";
LOG.warn("unknown local address, UPnP may fail");
}
return localHostIP;
}
}