/*
 * Copyright (c)2019 ZeroTier, Inc.
 *
 * Use of this software is governed by the Business Source License included
 * in the LICENSE.TXT file in the project's root directory.
 *
 * Change Date: 2026-01-01
 *
 * On the date above, in accordance with the Business Source License, use
 * of this software will be governed by version 2.0 of the Apache License.
 */
/****/

#ifdef ZT_USE_MINIUPNPC

// Uncomment to dump debug messages
//#define ZT_PORTMAPPER_TRACE 1

#ifdef __ANDROID__
#include <android/log.h>
#define PM_TRACE(...) ((void)__android_log_print(ANDROID_LOG_DEBUG, "PortMapper", __VA_ARGS__))
#else
#define PM_TRACE(...) fprintf(stderr, __VA_ARGS__)
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <string>

#include "../node/Utils.hpp"
#include "OSUtils.hpp"
#include "PortMapper.hpp"

// These must be defined to get rid of dynamic export stuff in libminiupnpc and libnatpmp
#ifdef __WINDOWS__
#ifndef MINIUPNP_STATICLIB
#define MINIUPNP_STATICLIB
#endif
#ifndef STATICLIB
#define STATICLIB
#endif
#endif

#ifdef ZT_USE_SYSTEM_MINIUPNPC
#include <miniupnpc/miniupnpc.h>
#include <miniupnpc/upnpcommands.h>
#else
#include "../ext/miniupnpc/miniupnpc.h"
#include "../ext/miniupnpc/upnpcommands.h"
#endif

#ifdef ZT_USE_SYSTEM_NATPMP
#include <natpmp.h>
#else
#ifdef __ANDROID__
#include "natpmp.h"
#else
#include "../ext/libnatpmp/natpmp.h"
#endif
#endif

namespace ZeroTier {

class PortMapperImpl
{
public:
	PortMapperImpl(int localUdpPortToMap,const char *un) :
		run(true),
		localPort(localUdpPortToMap),
		uniqueName(un)
	{
	}

	~PortMapperImpl() {}

	void threadMain()
		throw()
	{
		int mode = 0; // 0 == NAT-PMP, 1 == UPnP
		int retrytime = 500;

#ifdef ZT_PORTMAPPER_TRACE
		fprintf(stderr,"PortMapper: started for UDP port %d" ZT_EOL_S,localPort);
#endif

		while (run) {

			{
				// use initnatpmp to check if we can bind a port at all
				natpmp_t _natpmp;
				int result = initnatpmp(&_natpmp,0,0);
				if (result == NATPMP_ERR_CANNOTGETGATEWAY || result == NATPMP_ERR_SOCKETERROR) {
					closenatpmp(&_natpmp);
#ifdef ZT_PORTMAPPER_TRACE
					PM_TRACE("PortMapper: init failed %d. You might not have an internet connection yet. Trying again in %d" ZT_EOL_S, result, retrytime);
#endif
					Thread::sleep(retrytime);
					retrytime = retrytime * 2;
					if (retrytime > ZT_PORTMAPPER_REFRESH_DELAY / 10) {
						retrytime = ZT_PORTMAPPER_REFRESH_DELAY / 10;
					}
					continue;
				} else {
					closenatpmp(&_natpmp);
					retrytime = 500;
				}
			}
			// ---------------------------------------------------------------------
			// NAT-PMP mode (preferred)
			// ---------------------------------------------------------------------
			if (mode == 0) {
			  natpmp_t natpmp;
			  natpmpresp_t response;
				int r = 0;

				bool natPmpSuccess = false;
				for(int tries=0;tries<60;++tries) {
					int tryPort = (int)localPort + tries;
					if (tryPort >= 65535)
						tryPort = (tryPort - 65535) + 1025;

					memset(&natpmp,0,sizeof(natpmp));
					memset(&response,0,sizeof(response));

					if (initnatpmp(&natpmp,0,0) != 0) {
						mode = 1;
						closenatpmp(&natpmp);
#ifdef ZT_PORTMAPPER_TRACE
                        PM_TRACE("PortMapper: NAT-PMP: init failed, switching to UPnP mode" ZT_EOL_S);
#endif
						break;
					}

					InetAddress publicAddress;
					sendpublicaddressrequest(&natpmp);
					int64_t myTimeout = OSUtils::now() + 5000;
					do {
						fd_set fds;
						struct timeval timeout;
						FD_ZERO(&fds);
						FD_SET(natpmp.s, &fds);
						getnatpmprequesttimeout(&natpmp, &timeout);
						select(FD_SETSIZE, &fds, NULL, NULL, &timeout);
						r = readnatpmpresponseorretry(&natpmp, &response);
						if (OSUtils::now() >= myTimeout)
							break;
					} while (r == NATPMP_TRYAGAIN);
					if (r == 0) {
						publicAddress = InetAddress((uint32_t)response.pnu.publicaddress.addr.s_addr,0);
					} else {
#ifdef ZT_PORTMAPPER_TRACE
                        PM_TRACE("PortMapper: NAT-PMP: request for external address failed, aborting..." ZT_EOL_S);
#endif
						closenatpmp(&natpmp);
						break;
					}

				  sendnewportmappingrequest(&natpmp,NATPMP_PROTOCOL_UDP,localPort,tryPort,(ZT_PORTMAPPER_REFRESH_DELAY * 2) / 1000);
					myTimeout = OSUtils::now() + 10000;
					do {
				    fd_set fds;
				    struct timeval timeout;
				    FD_ZERO(&fds);
				    FD_SET(natpmp.s, &fds);
				    getnatpmprequesttimeout(&natpmp, &timeout);
				    select(FD_SETSIZE, &fds, NULL, NULL, &timeout);
				    r = readnatpmpresponseorretry(&natpmp, &response);
						if (OSUtils::now() >= myTimeout)
							break;
				  } while (r == NATPMP_TRYAGAIN);
					if (r == 0) {
						publicAddress.setPort(response.pnu.newportmapping.mappedpublicport);
#ifdef ZT_PORTMAPPER_TRACE
                        char paddr[128];
                        PM_TRACE("PortMapper: NAT-PMP: mapped %u to %s" ZT_EOL_S,(unsigned int)localPort,publicAddress.toString(paddr));
#endif
						Mutex::Lock sl(surface_l);
						surface.clear();
						surface.push_back(publicAddress);
						natPmpSuccess = true;
						closenatpmp(&natpmp);
						break;
					} else {
						closenatpmp(&natpmp);
						// continue
					}
				}

				if (!natPmpSuccess) {
					mode = 1;
#ifdef ZT_PORTMAPPER_TRACE
                    PM_TRACE("PortMapper: NAT-PMP: request failed, switching to UPnP mode" ZT_EOL_S);
#endif
					continue;
				}
			}
			// ---------------------------------------------------------------------

			// ---------------------------------------------------------------------
			// UPnP mode
			// ---------------------------------------------------------------------
			if (mode == 1) {
				char lanaddr[4096];
				char externalip[4096]; // no range checking? so make these buffers larger than any UDP packet a uPnP server could send us as a precaution :P
				char inport[16];
				char outport[16];
				struct UPNPUrls urls;
				struct IGDdatas data;

				int upnpError = 0;
				UPNPDev *devlist = upnpDiscoverAll(5000,(const char *)0,(const char *)0,0,0,2,&upnpError);
				if (devlist) {

#ifdef ZT_PORTMAPPER_TRACE
					{
						UPNPDev *dev = devlist;
						while (dev) {
                            PM_TRACE("PortMapper: found UPnP device at URL '%s': %s" ZT_EOL_S,dev->descURL,dev->st);
							dev = dev->pNext;
						}
					}
#endif

					memset(lanaddr,0,sizeof(lanaddr));
					memset(externalip,0,sizeof(externalip));
					memset(&urls,0,sizeof(urls));
					memset(&data,0,sizeof(data));
					OSUtils::ztsnprintf(inport,sizeof(inport),"%d",localPort);

					int foundValidIGD = 0;
					if ((foundValidIGD = UPNP_GetValidIGD(devlist,&urls,&data,lanaddr,sizeof(lanaddr)))&&(lanaddr[0])) {
#ifdef ZT_PORTMAPPER_TRACE
                        PM_TRACE("PortMapper: UPnP: my LAN IP address: %s" ZT_EOL_S,lanaddr);
#endif
						if ((UPNP_GetExternalIPAddress(urls.controlURL,data.first.servicetype,externalip) == UPNPCOMMAND_SUCCESS)&&(externalip[0])) {
#ifdef ZT_PORTMAPPER_TRACE
                            PM_TRACE("PortMapper: UPnP: my external IP address: %s" ZT_EOL_S,externalip);
#endif

							for(int tries=0;tries<60;++tries) {
								int tryPort = (int)localPort + tries;
								if (tryPort >= 65535)
									tryPort = (tryPort - 65535) + 1025;
								OSUtils::ztsnprintf(outport,sizeof(outport),"%u",tryPort);

								// First check and see if this port is already mapped to the
								// same unique name. If so, keep this mapping and don't try
								// to map again since this can break buggy routers. But don't
								// fail if this command fails since not all routers support it.
								{
									char haveIntClient[128]; // 128 == big enough for all these as per miniupnpc "documentation"
									char haveIntPort[128];
									char haveDesc[128];
									char haveEnabled[128];
									char haveLeaseDuration[128];
									memset(haveIntClient,0,sizeof(haveIntClient));
									memset(haveIntPort,0,sizeof(haveIntPort));
									memset(haveDesc,0,sizeof(haveDesc));
									memset(haveEnabled,0,sizeof(haveEnabled));
									memset(haveLeaseDuration,0,sizeof(haveLeaseDuration));
									if ((UPNP_GetSpecificPortMappingEntry(urls.controlURL,data.first.servicetype,outport,"UDP",(const char *)0,haveIntClient,haveIntPort,haveDesc,haveEnabled,haveLeaseDuration) == UPNPCOMMAND_SUCCESS)&&(uniqueName == haveDesc)) {
#ifdef ZT_PORTMAPPER_TRACE
                                        PM_TRACE("PortMapper: UPnP: reusing previously reserved external port: %s" ZT_EOL_S,outport);
#endif
										Mutex::Lock sl(surface_l);
										surface.clear();
										InetAddress tmp(externalip);
										tmp.setPort(tryPort);
										surface.push_back(tmp);
										break;
									}
								}

								// Try to map this port
								int mapResult = 0;
								if ((mapResult = UPNP_AddPortMapping(urls.controlURL,data.first.servicetype,outport,inport,lanaddr,uniqueName.c_str(),"UDP",(const char *)0,"0")) == UPNPCOMMAND_SUCCESS) {
#ifdef ZT_PORTMAPPER_TRACE
                                    PM_TRACE("PortMapper: UPnP: reserved external port: %s" ZT_EOL_S,outport);
#endif
									Mutex::Lock sl(surface_l);
									surface.clear();
									InetAddress tmp(externalip);
									tmp.setPort(tryPort);
									surface.push_back(tmp);
									break;
								} else {
#ifdef ZT_PORTMAPPER_TRACE
                                    PM_TRACE("PortMapper: UPnP: UPNP_AddPortMapping(%s) failed: %d" ZT_EOL_S,outport,mapResult);
#endif
									Thread::sleep(1000);
								}
							}

						} else {
							mode = 0;
#ifdef ZT_PORTMAPPER_TRACE
                            PM_TRACE("PortMapper: UPnP: UPNP_GetExternalIPAddress failed, returning to NAT-PMP mode" ZT_EOL_S);
#endif
						}
					} else {
						mode = 0;
#ifdef ZT_PORTMAPPER_TRACE
                        PM_TRACE("PortMapper: UPnP: UPNP_GetValidIGD failed, returning to NAT-PMP mode" ZT_EOL_S);
#endif
					}
					freeUPNPDevlist(devlist);

					if(foundValidIGD) {
						FreeUPNPUrls(&urls);
					}
				} else {
					mode = 0;
#ifdef ZT_PORTMAPPER_TRACE
                    PM_TRACE("PortMapper: upnpDiscover failed, returning to NAT-PMP mode: %d" ZT_EOL_S,upnpError);
#endif
				}
			}
			// ---------------------------------------------------------------------

#ifdef ZT_PORTMAPPER_TRACE
            PM_TRACE("UPNPClient: rescanning in %d ms" ZT_EOL_S,ZT_PORTMAPPER_REFRESH_DELAY);
#endif
			Thread::sleep(ZT_PORTMAPPER_REFRESH_DELAY);
		}

		delete this;
	}

	volatile bool run;
	int localPort;
	std::string uniqueName;

	Mutex surface_l;
	std::vector<InetAddress> surface;
};

PortMapper::PortMapper(int localUdpPortToMap,const char *uniqueName)
{
	_impl = new PortMapperImpl(localUdpPortToMap,uniqueName);
	Thread::start(_impl);
}

PortMapper::~PortMapper()
{
	_impl->run = false;
}

std::vector<InetAddress> PortMapper::get() const
{
	Mutex::Lock _l(_impl->surface_l);
	return _impl->surface;
}

} // namespace ZeroTier

#endif // ZT_USE_MINIUPNPC
