upnpctrl: add command to show all existing port mappings

This can be useful for debugging purposes.

Change-Id: I9da11d20a7a8cd9f7d1eae9d4aee45281c5cd4ad
diff --git a/include/upnp/mapping.h b/include/upnp/mapping.h
index e92a654..b9c2078 100644
--- a/include/upnp/mapping.h
+++ b/include/upnp/mapping.h
@@ -137,5 +137,17 @@
 #endif
 };
 
+struct MappingInfo
+{
+    std::string remoteHost;
+    std::string protocol;
+    std::string internalClient;
+    std::string enabled;
+    std::string description;
+    uint16_t externalPort;
+    uint16_t internalPort;
+    uint32_t leaseDuration;
+};
+
 } // namespace upnp
 } // namespace dhtnet
diff --git a/include/upnp/upnp_context.h b/include/upnp/upnp_context.h
index 9941e3b..0958864 100644
--- a/include/upnp/upnp_context.h
+++ b/include/upnp/upnp_context.h
@@ -57,6 +57,14 @@
 class UPnPProtocol;
 class IGD;
 
+struct IGDInfo
+{
+    std::string uid;
+    IpAddr localIp;
+    IpAddr publicIp;
+    std::vector<MappingInfo> mappingInfoList;
+};
+
 enum class UpnpIgdEvent { ADDED, REMOVED, INVALID_STATE };
 
 // Interface used to report mapping event from the protocol implementations.
@@ -136,6 +144,11 @@
     // Generate random port numbers
     static uint16_t generateRandomPort(PortType type, bool mustBeEven = false);
 
+    // Return information about the UPnPContext's valid IGDs, including the list
+    // of all existing port mappings (for IGDs which support a protocol that allows
+    // querying that information -- UPnP does, but NAT-PMP doesn't for example)
+    std::vector<IGDInfo> getIgdsInfo() const;
+
     template <typename T>
     inline void dispatch(T&& f) {
         ctx->dispatch(std::move(f));
diff --git a/src/upnp/protocol/pupnp/pupnp.cpp b/src/upnp/protocol/pupnp/pupnp.cpp
index 432ea60..dbb05f1 100644
--- a/src/upnp/protocol/pupnp/pupnp.cpp
+++ b/src/upnp/protocol/pupnp/pupnp.cpp
@@ -1353,6 +1353,85 @@
     return mapList;
 }
 
+std::vector<MappingInfo>
+PUPnP::getMappingsInfo(const std::shared_ptr<IGD>& igd) const
+{
+    auto upnpIgd = std::dynamic_pointer_cast<UPnPIGD>(igd);
+    assert(upnpIgd);
+
+    std::vector<MappingInfo> mappingInfoList;
+
+    if (not clientRegistered_ or not upnpIgd->isValid() or not upnpIgd->getLocalIp())
+        return mappingInfoList;
+
+    static constexpr const char* action_name {"GetGenericPortMappingEntry"};
+
+    for (int entry_idx = 0;; entry_idx++) {
+        std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
+            action(nullptr, ixmlDocument_free); // Action pointer.
+        IXML_Document* action_container_ptr = nullptr;
+
+        std::unique_ptr<IXML_Document, decltype(ixmlDocument_free)&>
+            response(nullptr, ixmlDocument_free); // Response pointer.
+        IXML_Document* response_container_ptr = nullptr;
+
+        UpnpAddToAction(&action_container_ptr,
+                        action_name,
+                        upnpIgd->getServiceType().c_str(),
+                        "NewPortMappingIndex",
+                        std::to_string(entry_idx).c_str());
+        action.reset(action_container_ptr);
+
+        int upnp_err = UpnpSendAction(ctrlptHandle_,
+                                      upnpIgd->getControlURL().c_str(),
+                                      upnpIgd->getServiceType().c_str(),
+                                      nullptr,
+                                      action.get(),
+                                      &response_container_ptr);
+        response.reset(response_container_ptr);
+
+        if (!response || upnp_err != UPNP_E_SUCCESS) {
+            break;
+        }
+
+        auto errorCode = getFirstDocItem(response.get(), "errorCode");
+        if (not errorCode.empty()) {
+            auto error = to_int<int>(errorCode);
+            if (error == ARRAY_IDX_INVALID or error == CONFLICT_IN_MAPPING) {
+                // No more port mapping entries in the response.
+                break;
+            } else {
+                auto errorDescription = getFirstDocItem(response.get(), "errorDescription");
+                if (logger_) logger_->error("PUPnP: GetGenericPortMappingEntry returned with error: {:s}: {:s}",
+                         errorCode,
+                         errorDescription);
+                break;
+            }
+        }
+
+        // Parse the response.
+        MappingInfo info;
+        info.remoteHost = getFirstDocItem(response.get(), "NewRemoteHost");
+        info.protocol = getFirstDocItem(response.get(), "NewProtocol");
+        info.internalClient = getFirstDocItem(response.get(), "NewInternalClient");
+        info.enabled = getFirstDocItem(response.get(), "NewEnabled");
+        info.description = getFirstDocItem(response.get(), "NewPortMappingDescription");
+
+        auto externalPort = getFirstDocItem(response.get(), "NewExternalPort");
+        info.externalPort = to_int<uint16_t>(externalPort);
+
+        auto internalPort = getFirstDocItem(response.get(), "NewInternalPort");
+        info.internalPort = to_int<uint16_t>(internalPort);
+
+        auto leaseDuration = getFirstDocItem(response.get(), "NewLeaseDuration");
+        info.leaseDuration = to_int<uint32_t>(leaseDuration);
+
+        mappingInfoList.push_back(std::move(info));
+    }
+
+    return mappingInfoList;
+}
+
 void
 PUPnP::deleteMappingsByDescription(const std::shared_ptr<IGD>& igd, const std::string& description)
 {
diff --git a/src/upnp/protocol/pupnp/pupnp.h b/src/upnp/protocol/pupnp/pupnp.h
index 5bba6dc..e29e01d 100644
--- a/src/upnp/protocol/pupnp/pupnp.h
+++ b/src/upnp/protocol/pupnp/pupnp.h
@@ -93,6 +93,9 @@
     std::map<Mapping::key_t, Mapping> getMappingsListByDescr(
         const std::shared_ptr<IGD>& igd, const std::string& descr) const override;
 
+    // Get information about all existing port mappings on the given IGD
+    std::vector<MappingInfo> getMappingsInfo(const std::shared_ptr<IGD>& igd) const override;
+
     // Request a new mapping.
     void requestMappingAdd(const Mapping& mapping) override;
 
diff --git a/src/upnp/protocol/upnp_protocol.h b/src/upnp/protocol/upnp_protocol.h
index 3dde4ab..1ec05ac 100644
--- a/src/upnp/protocol/upnp_protocol.h
+++ b/src/upnp/protocol/upnp_protocol.h
@@ -77,6 +77,12 @@
         return {};
     }
 
+    // Get information about all existing port mappings on the given IGD
+    virtual std::vector<MappingInfo> getMappingsInfo(const std::shared_ptr<IGD>& igd) const
+    {
+        return {};
+    }
+
     // Sends a request to add a mapping.
     virtual void requestMappingAdd(const Mapping& map) = 0;
 
diff --git a/src/upnp/upnp_context.cpp b/src/upnp/upnp_context.cpp
index 78488dc..7ea3d83 100644
--- a/src/upnp/upnp_context.cpp
+++ b/src/upnp/upnp_context.cpp
@@ -459,6 +459,27 @@
     }
 }
 
+std::vector<IGDInfo>
+UPnPContext::getIgdsInfo() const
+{
+    std::vector<IGDInfo> igdInfoList;
+
+    std::lock_guard lk(mappingMutex_);
+    for (auto& igd : validIgdList_) {
+        auto protocol = protocolList_.at(igd->getProtocol());
+
+        IGDInfo info;
+        info.uid = igd->getUID();
+        info.localIp = igd->getLocalIp();
+        info.publicIp = igd->getPublicIp();
+        info.mappingInfoList = protocol->getMappingsInfo(igd);
+
+        igdInfoList.push_back(std::move(info));
+    }
+
+    return igdInfoList;
+}
+
 uint16_t
 UPnPContext::getAvailablePortNumber(PortType type)
 {
diff --git a/tools/upnp/upnpctrl.cpp b/tools/upnp/upnpctrl.cpp
index 586898b..a5034a6 100644
--- a/tools/upnp/upnpctrl.cpp
+++ b/tools/upnp/upnpctrl.cpp
@@ -14,7 +14,38 @@
                     "  help, h, ?\n"
                     "  quit, exit, q, x\n"
                     "  ip\n"
-                    "  open <port> <protocol>\n");
+                    "  open <port> <protocol>\n"
+                    "  close <port>\n"
+                    "  mappings\n");
+}
+
+void
+print_mappings(std::shared_ptr<dhtnet::upnp::UPnPContext> upnpContext)
+{
+    for (auto const& igdInfo : upnpContext->getIgdsInfo()) {
+        fmt::print("\nIGD: \"{}\" [local IP: {} - public IP: {}]\n",
+                   igdInfo.uid,
+                   igdInfo.localIp.toString(),
+                   igdInfo.publicIp.toString());
+
+        if (igdInfo.mappingInfoList.empty())
+            continue;
+
+        static const char *format = "{:>8} {:>12} {:>12} {:>8} {:>8} {:>16} {:>16}  {}\n";
+        fmt::print(format, "Protocol", "ExternalPort", "InternalPort", "Duration",
+                   "Enabled?", "InternalClient", "RemoteHost", "Description");
+        for (auto const& mappingInfo : igdInfo.mappingInfoList) {
+            fmt::print(format,
+                       mappingInfo.protocol,
+                       mappingInfo.externalPort,
+                       mappingInfo.internalPort,
+                       mappingInfo.leaseDuration,
+                       mappingInfo.enabled,
+                       mappingInfo.internalClient,
+                       mappingInfo.remoteHost.empty() ? "any" : mappingInfo.remoteHost,
+                       mappingInfo.description);
+        }
+    }
 }
 
 std::string to_lower(std::string_view str_v) {
@@ -82,11 +113,13 @@
                     ++it;
                 }
             }
+        } else if (command == "mappings") {
+            print_mappings(upnpContext);
         } else {
             fmt::print("Unknown command: {}\n", command);
         }
     }
-    fmt::print("Stopping...");
+    fmt::print("Stopping...\n");
     for (auto c: mappings)
         controller->releaseMapping(*c);
     mappings.clear();