/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 *
 * (c) ZeroTier, Inc.
 * https://www.zerotier.com/
 */

#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 "../node/Utils.hpp"
#include "OSUtils.hpp"
#include "PortMapper.hpp"

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

// 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
