View Javadoc
1   /*
2    * Copyright (C) 2013 4th Line GmbH, Switzerland
3    *
4    * The contents of this file are subject to the terms of either the GNU
5    * Lesser General Public License Version 2 or later ("LGPL") or the
6    * Common Development and Distribution License Version 1 or later
7    * ("CDDL") (collectively, the "License"). You may not use this file
8    * except in compliance with the License. See LICENSE.txt for more
9    * information.
10   *
11   * This program is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14   */
15  
16  package org.fourthline.cling.support.igd;
17  
18  import org.fourthline.cling.model.action.ActionInvocation;
19  import org.fourthline.cling.model.message.UpnpResponse;
20  import org.fourthline.cling.model.meta.Device;
21  import org.fourthline.cling.model.meta.Service;
22  import org.fourthline.cling.model.types.DeviceType;
23  import org.fourthline.cling.model.types.ServiceType;
24  import org.fourthline.cling.model.types.UDADeviceType;
25  import org.fourthline.cling.model.types.UDAServiceType;
26  import org.fourthline.cling.registry.DefaultRegistryListener;
27  import org.fourthline.cling.registry.Registry;
28  import org.fourthline.cling.support.igd.callback.PortMappingAdd;
29  import org.fourthline.cling.support.igd.callback.PortMappingDelete;
30  import org.fourthline.cling.support.model.PortMapping;
31  
32  import java.util.ArrayList;
33  import java.util.HashMap;
34  import java.util.Iterator;
35  import java.util.List;
36  import java.util.Map;
37  import java.util.logging.Logger;
38  
39  /**
40   * Maintains UPnP port mappings on an InternetGatewayDevice automatically.
41   * <p>
42   * This listener will wait for discovered devices which support either
43   * {@code WANIPConnection} or the {@code WANPPPConnection} service. As soon as any such
44   * service is discovered, the desired port mapping will be created. When the UPnP service
45   * is shutting down, all previously established port mappings with all services will
46   * be deleted.
47   * </p>
48   * <p>
49   * The following listener maps external WAN TCP port 8123 to internal host 10.0.0.2:
50   * </p>
51   * <pre>{@code
52   * upnpService.getRegistry().addListener(
53   *newPortMappingListener(newPortMapping(8123, "10.0.0.2",PortMapping.Protocol.TCP))
54   * );}</pre>
55   * <p>
56   * If all you need from the Cling UPnP stack is NAT port mapping, use the following idiom:
57   * </p>
58   * <pre>{@code
59   * UpnpService upnpService = new UpnpServiceImpl(
60   *     new PortMappingListener(new PortMapping(8123, "10.0.0.2", PortMapping.Protocol.TCP))
61   * );
62   * <p/>
63   * upnpService.getControlPoint().search(new STAllHeader()); // Search for all devices
64   * <p/>
65   * upnpService.shutdown(); // When you no longer need the port mapping
66   * }</pre>
67   *
68   * @author Christian Bauer
69   */
70  public class PortMappingListener extends DefaultRegistryListener {
71  
72      private static final Logger log = Logger.getLogger(PortMappingListener.class.getName());
73  
74      public static final DeviceType IGD_DEVICE_TYPE = new UDADeviceType("InternetGatewayDevice", 1);
75      public static final DeviceType CONNECTION_DEVICE_TYPE = new UDADeviceType("WANConnectionDevice", 1);
76  
77      public static final ServiceType IP_SERVICE_TYPE = new UDAServiceType("WANIPConnection", 1);
78      public static final ServiceType PPP_SERVICE_TYPE = new UDAServiceType("WANPPPConnection", 1);
79  
80      protected PortMapping[] portMappings;
81  
82      // The key of the map is Service and equality is object identity, this is by-design
83      protected Map<Service, List<PortMapping>> activePortMappings = new HashMap<>();
84  
85      public PortMappingListener(PortMapping portMapping) {
86          this(new PortMapping[]{portMapping});
87      }
88  
89      public PortMappingListener(PortMapping[] portMappings) {
90          this.portMappings = portMappings;
91      }
92  
93      @Override
94      synchronized public void deviceAdded(Registry registry, Device device) {
95  
96          Service connectionService;
97          if ((connectionService = discoverConnectionService(device)) == null) return;
98  
99          log.fine("Activating port mappings on: " + connectionService);
100 
101         final List<PortMapping> activeForService = new ArrayList<>();
102         for (final PortMapping pm : portMappings) {
103             new PortMappingAdd(connectionService, registry.getUpnpService().getControlPoint(), pm) {
104 
105                 @Override
106                 public void success(ActionInvocation invocation) {
107                     log.fine("Port mapping added: " + pm);
108                     activeForService.add(pm);
109                 }
110 
111                 @Override
112                 public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
113                     handleFailureMessage("Failed to add port mapping: " + pm);
114                     handleFailureMessage("Reason: " + defaultMsg);
115                 }
116             }.run(); // Synchronous!
117         }
118 
119         activePortMappings.put(connectionService, activeForService);
120     }
121 
122     @Override
123     synchronized public void deviceRemoved(Registry registry, Device device) {
124         for (Service service : device.findServices()) {
125             Iterator<Map.Entry<Service, List<PortMapping>>> it = activePortMappings.entrySet().iterator();
126             while (it.hasNext()) {
127                 Map.Entry<Service, List<PortMapping>> activeEntry = it.next();
128                 if (!activeEntry.getKey().equals(service)) continue;
129 
130                 if (activeEntry.getValue().size() > 0)
131                     handleFailureMessage("Device disappeared, couldn't delete port mappings: " + activeEntry.getValue().size());
132 
133                 it.remove();
134             }
135         }
136     }
137 
138     @Override
139     synchronized public void beforeShutdown(Registry registry) {
140         for (Map.Entry<Service, List<PortMapping>> activeEntry : activePortMappings.entrySet()) {
141 
142             final Iterator<PortMapping> it = activeEntry.getValue().iterator();
143             while (it.hasNext()) {
144                 final PortMapping pm = it.next();
145                 log.fine("Trying to delete port mapping on IGD: " + pm);
146                 new PortMappingDelete(activeEntry.getKey(), registry.getUpnpService().getControlPoint(), pm) {
147 
148                     @Override
149                     public void success(ActionInvocation invocation) {
150                         log.fine("Port mapping deleted: " + pm);
151                         it.remove();
152                     }
153 
154                     @Override
155                     public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
156                         handleFailureMessage("Failed to delete port mapping: " + pm);
157                         handleFailureMessage("Reason: " + defaultMsg);
158                     }
159 
160                 }.run(); // Synchronous!
161             }
162         }
163     }
164 
165     protected Service discoverConnectionService(Device device) {
166         if (!device.getType().equals(IGD_DEVICE_TYPE)) {
167             return null;
168         }
169 
170         Device[] connectionDevices = device.findDevices(CONNECTION_DEVICE_TYPE);
171         if (connectionDevices.length == 0) {
172             log.fine("IGD doesn't support '" + CONNECTION_DEVICE_TYPE + "': " + device);
173             return null;
174         }
175 
176         Device connectionDevice = connectionDevices[0];
177         log.fine("Using first discovered WAN connection device: " + connectionDevice);
178 
179         Service ipConnectionService = connectionDevice.findService(IP_SERVICE_TYPE);
180         Service pppConnectionService = connectionDevice.findService(PPP_SERVICE_TYPE);
181 
182         if (ipConnectionService == null && pppConnectionService == null) {
183             log.fine("IGD doesn't support IP or PPP WAN connection service: " + device);
184         }
185 
186         return ipConnectionService != null ? ipConnectionService : pppConnectionService;
187     }
188 
189     protected void handleFailureMessage(String s) {
190         log.warning(s);
191     }
192 
193 }
194