/**
* UPnP PortMapper - A tool for managing port forwardings via UPnP
* Copyright (C) 2015 Christoph Pirkl <christoph at users.sourceforge.net>
*
* 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 3 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, see <http://www.gnu.org/licenses/>.
*/
package org.chris.portmapper.router.cling;
import java.util.Collection;
import java.util.LinkedList;
import org.chris.portmapper.model.PortMapping;
import org.chris.portmapper.router.RouterException;
import org.chris.portmapper.router.cling.action.ActionService;
import org.chris.portmapper.router.cling.action.GetPortMappingEntryAction;
import org.fourthline.cling.model.message.control.IncomingActionResponseMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.sbbi.upnp.impls.InternetGatewayDevice;
/**
* This class fetches all {@link PortMapping} from an {@link InternetGatewayDevice}.
*/
class ClingPortMappingExtractor {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final Collection<PortMapping> mappings;
private boolean moreEntries;
private int currentMappingNumber;
/**
* The maximum number of port mappings that we will try to retrieve from the router.
*/
private final int maxNumPortMappings;
private final ActionService actionService;
ClingPortMappingExtractor(final ActionService actionService, final int maxNumPortMappings) {
this.actionService = actionService;
this.maxNumPortMappings = maxNumPortMappings;
this.mappings = new LinkedList<>();
this.moreEntries = true;
this.currentMappingNumber = 0;
}
public Collection<PortMapping> getPortMappings() throws RouterException {
/*
* This is a little trick to get all port mappings. There is a method that gets the number of available port
* mappings (getNatMappingsCount()), but it seems, that this method just tries to get all port mappings and
* checks, if an error is returned.
*
* In order to speed this up, we will do the same here, but stop, when the first exception is thrown.
*/
while (morePortMappingsAvailable()) {
logger.debug("Getting port mapping with entry number " + currentMappingNumber + "...");
try {
final PortMapping portMapping = actionService
.run(new GetPortMappingEntryAction(actionService.getService(), currentMappingNumber));
mappings.add(portMapping);
} catch (final ClingOperationFailedException e) {
handleFailureResponse(e.getResponse());
}
currentMappingNumber++;
}
checkMaxNumPortMappingsReached();
return mappings;
}
/**
* Check, if the max number of entries is reached and print a warning message.
*/
private void checkMaxNumPortMappingsReached() {
if (currentMappingNumber == maxNumPortMappings) {
logger.warn(
"Reached max number of port mappings to get ({}). Perhaps not all port mappings where retrieved.",
maxNumPortMappings);
}
}
private boolean morePortMappingsAvailable() {
return moreEntries && currentMappingNumber < maxNumPortMappings;
}
private void handleFailureResponse(final IncomingActionResponseMessage incomingActionResponseMessage) {
if (isNoMoreMappingsException(incomingActionResponseMessage)) {
moreEntries = false;
logger.debug("Got no port mapping for entry number {} (status: {}). Stop getting more entries.",
currentMappingNumber, incomingActionResponseMessage.getOperation().getStatusMessage());
} else {
moreEntries = false;
logger.info(
"Got error response when fetching port mapping for entry number {}: '{}'. Stop getting more entries.",
currentMappingNumber, incomingActionResponseMessage);
}
}
/**
* This method checks, if the error code of the given exception means, that no more mappings are available.
* <p>
* The following error codes are recognized:
* <ul>
* <li>SpecifiedArrayIndexInvalid: 713</li>
* <li>NoSuchEntryInArray: 714</li>
* <li>Invalid Args: 402 (e.g. for DD-WRT, TP-LINK TL-R460 firmware 4.7.6 Build 100714 Rel.63134n)</li>
* <li>Other errors, e.g. "The reference to entity "T" must end with the ';' delimiter" or
* "Content is not allowed in prolog": 899 (e.g. ActionTec MI424-WR, Thomson TWG850-4U)</li>
* </ul>
* See bug reports
* <ul>
* <li><a href= "https://sourceforge.net/tracker/index.php?func=detail&aid=1939749&group_id=213879&atid=1027466" >
* https://sourceforge.net/tracker/index.php?func=detail&aid= 1939749&group_id=213879&atid=1027466</a></li>
* <li><a href="http://www.sbbi.net/forum/viewtopic.php?p=394">http://www.sbbi .net/forum/viewtopic.php?p=394</a>
* </li>
* <li><a href= "http://sourceforge.net/tracker/?func=detail&atid=1027466&aid=3325388&group_id=213879" >http://
* sourceforge.net/tracker/?func=detail&atid=1027466&aid=3325388& group_id=213879</a></li>
* <a href= "https://sourceforge.net/tracker2/?func=detail&aid=2540478&group_id=213879&atid=1027466" >https://
* sourceforge.net/tracker2/?func=detail&aid=2540478&group_id= 213879&atid=1027466</a></li>
* </ul>
*
* @param incomingActionResponseMessage
* the exception to check
* @return <code>true</code>, if the given exception means, that no more port mappings are available, else
* <code>false</code>.
*/
private boolean isNoMoreMappingsException(final IncomingActionResponseMessage incomingActionResponseMessage) {
final int errorCode = incomingActionResponseMessage.getOperation().getStatusCode();
switch (errorCode) {
case 713:
case 714:
case 402:
case 899:
return true;
default:
return false;
}
}
}