Browse Source

Merge branch 'zeroidc' into dev

Adam Ierymenko 3 years ago
parent
commit
f8d7796099

+ 2 - 0
.gitignore

@@ -122,6 +122,8 @@ attic/world/*.c25519
 attic/world/mkworld
 workspace/
 workspace2/
+zeroidc/target/
+tmp/
 
 #snapcraft specifics
 /parts/

+ 1 - 1
controller/ConnectionPool.hpp

@@ -95,7 +95,7 @@ public:
 
         if(m_pool.size()==0){
             
-            if ((m_pool.size() + m_borrowed.size()) <= m_maxPoolSize) {
+            if ((m_pool.size() + m_borrowed.size()) < m_maxPoolSize) {
                 try {
                     std::shared_ptr<Connection> conn = m_factory->create();
                     m_borrowed.insert(conn);

+ 27 - 1
controller/DB.hpp

@@ -40,6 +40,32 @@
 namespace ZeroTier
 {
 
+struct AuthInfo
+{
+public:
+	AuthInfo() 
+	: enabled(false)
+	, version(0)
+	, authenticationURL()
+	, authenticationExpiryTime(0)
+	, issuerURL()
+	, centralAuthURL()
+	, ssoNonce()
+	, ssoState()
+	, ssoClientID()
+	{}
+
+	bool enabled;
+	uint64_t version;
+	std::string authenticationURL;
+	uint64_t authenticationExpiryTime;
+	std::string issuerURL;
+	std::string centralAuthURL;
+	std::string ssoNonce;
+	std::string ssoState;
+	std::string ssoClientID;
+};
+
 /**
  * Base class with common infrastructure for all controller DB implementations
  */
@@ -108,7 +134,7 @@ public:
 	virtual void eraseMember(const uint64_t networkId,const uint64_t memberId) = 0;
 	virtual void nodeIsOnline(const uint64_t networkId,const uint64_t memberId,const InetAddress &physicalAddress) = 0;
 
-	virtual std::string getSSOAuthURL(const nlohmann::json &member, const std::string &redirectURL) { return ""; }
+	virtual AuthInfo getSSOAuthInfo(const nlohmann::json &member, const std::string &redirectURL) { return AuthInfo(); }
 	virtual void networkMemberSSOHasExpired(uint64_t nwid, int64_t ts);
 
 	inline void addListener(DB::ChangeListener *const listener)

+ 5 - 5
controller/DBMirrorSet.cpp

@@ -125,16 +125,16 @@ bool DBMirrorSet::get(const uint64_t networkId,nlohmann::json &network,std::vect
 	return false;
 }
 
-std::string DBMirrorSet::getSSOAuthURL(const nlohmann::json &member, const std::string &redirectURL) 
+AuthInfo DBMirrorSet::getSSOAuthInfo(const nlohmann::json &member, const std::string &redirectURL) 
 {
 	std::lock_guard<std::mutex> l(_dbs_l);
 	for(auto d=_dbs.begin();d!=_dbs.end();++d) { 
-		std::string url = (*d)->getSSOAuthURL(member, redirectURL);
-		if (!url.empty()) {
-			return url;
+		AuthInfo info = (*d)->getSSOAuthInfo(member, redirectURL);
+		if (info.enabled) {
+			return info;
 		}
 	}
-	return "";
+	return AuthInfo();
 }
 
 void DBMirrorSet::networkMemberSSOHasExpired(uint64_t nwid, int64_t ts)

+ 1 - 1
controller/DBMirrorSet.hpp

@@ -51,7 +51,7 @@ public:
 	virtual void onNetworkMemberUpdate(const void *db,uint64_t networkId,uint64_t memberId,const nlohmann::json &member);
 	virtual void onNetworkMemberDeauthorize(const void *db,uint64_t networkId,uint64_t memberId);
 
-	std::string getSSOAuthURL(const nlohmann::json &member, const std::string &redirectURL);
+	AuthInfo getSSOAuthInfo(const nlohmann::json &member, const std::string &redirectURL);
 	void networkMemberSSOHasExpired(uint64_t nwid, int64_t ts);
 
 	inline void addDB(const std::shared_ptr<DB> &db)

+ 71 - 35
controller/EmbeddedNetworkController.cpp

@@ -63,29 +63,6 @@ namespace ZeroTier {
 
 namespace {
 
-std::string url_encode(const std::string &value) {
-    std::ostringstream escaped;
-    escaped.fill('0');
-    escaped << std::hex;
-
-    for (std::string::const_iterator i = value.begin(), n = value.end(); i != n; ++i) {
-        std::string::value_type c = (*i);
-
-        // Keep alphanumeric and other accepted characters intact
-        if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
-            escaped << c;
-            continue;
-        }
-
-        // Any other characters are percent-encoded
-        escaped << std::uppercase;
-        escaped << '%' << std::setw(2) << int((unsigned char) c);
-        escaped << std::nouppercase;
-    }
-
-    return escaped.str();
-}
-
 static json _renderRule(ZT_VirtualNetworkRule &rule)
 {
 	char tmp[128];
@@ -503,7 +480,7 @@ EmbeddedNetworkController::~EmbeddedNetworkController()
 }
 
 void EmbeddedNetworkController::setSSORedirectURL(const std::string &url) {
-	_ssoRedirectURL = url_encode(url);
+	_ssoRedirectURL = url;
 }
 
 void EmbeddedNetworkController::init(const Identity &signingId,Sender *sender)
@@ -1360,29 +1337,61 @@ void EmbeddedNetworkController::_request(
 	// Otherwise no, we use standard auth logic.
 	bool networkSSOEnabled = OSUtils::jsonBool(network["ssoEnabled"], false);
 	bool memberSSOExempt = OSUtils::jsonBool(member["ssoExempt"], false);
-	std::string authenticationURL;
+	AuthInfo info;
 	if (networkSSOEnabled && !memberSSOExempt) {
-		authenticationURL = _db.getSSOAuthURL(member, _ssoRedirectURL);
+		// TODO:  Get expiry time if auth is still valid
+
+		// else get new auth info & stuff
+		info = _db.getSSOAuthInfo(member, _ssoRedirectURL);
+		assert(info.enabled == networkSSOEnabled);
+
 		std::string memberId = member["id"];
 		//fprintf(stderr, "ssoEnabled && !ssoExempt %s-%s\n", nwids, memberId.c_str());
 		uint64_t authenticationExpiryTime = (int64_t)OSUtils::jsonInt(member["authenticationExpiryTime"], 0);
-		//fprintf(stderr, "authExpiryTime: %lld\n", authenticationExpiryTime);
+		fprintf(stderr, "authExpiryTime: %lld\n", authenticationExpiryTime);
 		if (authenticationExpiryTime < now) {
-			if (!authenticationURL.empty()) {
+			fprintf(stderr, "Handling expired member\n");
+			if (info.version == 0) {
+				if (!info.authenticationURL.empty()) {
+					_db.networkMemberSSOHasExpired(nwid, now);
+					onNetworkMemberDeauthorize(&_db, nwid, identity.address().toInt());
+
+					Dictionary<4096> authInfo;
+					authInfo.add(ZT_AUTHINFO_DICT_KEY_VERSION, (uint64_t)0ULL);
+					authInfo.add(ZT_AUTHINFO_DICT_KEY_AUTHENTICATION_URL, info.authenticationURL.c_str());
+					//fprintf(stderr, "sending auth URL: %s\n", authenticationURL.c_str());
+
+					DB::cleanMember(member);
+					_db.save(member,true);
+
+					_sender->ncSendError(nwid,requestPacketId,identity.address(),NetworkController::NC_ERROR_AUTHENTICATION_REQUIRED, authInfo.data(), authInfo.sizeBytes());
+					return;
+				}
+			}
+			else if (info.version == 1) {
 				_db.networkMemberSSOHasExpired(nwid, now);
 				onNetworkMemberDeauthorize(&_db, nwid, identity.address().toInt());
 
-				Dictionary<3072> authInfo;
-				authInfo.add("aU", authenticationURL.c_str());
-				//fprintf(stderr, "sending auth URL: %s\n", authenticationURL.c_str());
+				Dictionary<8192> authInfo;
+				authInfo.add(ZT_AUTHINFO_DICT_KEY_VERSION, info.version);
+				authInfo.add(ZT_AUTHINFO_DICT_KEY_ISSUER_URL, info.issuerURL.c_str());
+				authInfo.add(ZT_AUTHINFO_DICT_KEY_CENTRAL_ENDPOINT_URL, info.centralAuthURL.c_str());
+				authInfo.add(ZT_AUTHINFO_DICT_KEY_NONCE, info.ssoNonce.c_str());
+				authInfo.add(ZT_AUTHINFO_DICT_KEY_STATE, info.ssoState.c_str());
+				authInfo.add(ZT_AUTHINFO_DICT_KEY_CLIENT_ID, info.ssoClientID.c_str());
 
 				DB::cleanMember(member);
-				_db.save(member,true);
+				_db.save(member, true);
 
+				fprintf(stderr, "Sending NC_ERROR_AUTHENTICATION_REQUIRED\n");
 				_sender->ncSendError(nwid,requestPacketId,identity.address(),NetworkController::NC_ERROR_AUTHENTICATION_REQUIRED, authInfo.data(), authInfo.sizeBytes());
 				return;
 			}
+			else {
+				fprintf(stderr, "invalid sso info.version %llu\n", info.version);
+			}
 		} else if (authorized) {
+			fprintf(stderr, "Setting member will expire to: %lld\n", authenticationExpiryTime);
 			_db.memberWillExpire(authenticationExpiryTime, nwid, identity.address().toInt());
 		}
 	}
@@ -1452,9 +1461,36 @@ void EmbeddedNetworkController::_request(
 	nc->multicastLimit = (unsigned int)OSUtils::jsonInt(network["multicastLimit"],32ULL);
 
 	nc->ssoEnabled = OSUtils::jsonBool(network["ssoEnabled"], false);
-	nc->authenticationExpiryTime = OSUtils::jsonInt(member["authenticationExpiryTime"], 0LL);
-	if (!authenticationURL.empty())
-		Utils::scopy(nc->authenticationURL, sizeof(nc->authenticationURL), authenticationURL.c_str());
+	nc->ssoVersion = info.version;
+
+	if (info.version == 0) {
+		nc->authenticationExpiryTime = OSUtils::jsonInt(member["authenticationExpiryTime"], 0LL);
+		if (!info.authenticationURL.empty()) {
+			Utils::scopy(nc->authenticationURL, sizeof(nc->authenticationURL), info.authenticationURL.c_str());
+		}
+	}
+	else if (info.version == 1) {
+		nc->authenticationExpiryTime = OSUtils::jsonInt(member["authenticationExpiryTime"], 0LL);
+		if (!info.authenticationURL.empty()) {
+			Utils::scopy(nc->authenticationURL, sizeof(nc->authenticationURL), info.authenticationURL.c_str());
+		}
+		if (!info.centralAuthURL.empty()) {
+			Utils::scopy(nc->centralAuthURL, sizeof(nc->centralAuthURL), info.centralAuthURL.c_str());
+		}
+		if (!info.issuerURL.empty()) {
+			fprintf(stderr, "copying issuerURL to nc: %s\n", info.issuerURL.c_str());
+			Utils::scopy(nc->issuerURL, sizeof(nc->issuerURL), info.issuerURL.c_str());
+		}
+		if (!info.ssoNonce.empty()) {
+			Utils::scopy(nc->ssoNonce, sizeof(nc->ssoNonce), info.ssoNonce.c_str());
+		}
+		if (!info.ssoState.empty()) {
+			Utils::scopy(nc->ssoState, sizeof(nc->ssoState), info.ssoState.c_str());
+		}
+		if (!info.ssoClientID.empty()) {
+			Utils::scopy(nc->ssoClientID, sizeof(nc->ssoClientID), info.ssoClientID.c_str());
+		}
+	}
 
 	std::string rtt(OSUtils::jsonString(member["remoteTraceTarget"],""));
 	if (rtt.length() == 10) {

+ 165 - 107
controller/PostgreSQL.cpp

@@ -23,6 +23,7 @@
 
 #include <libpq-fe.h>
 #include <sstream>
+#include <iomanip>
 #include <climits>
 #include <chrono>
 
@@ -80,6 +81,28 @@ std::vector<std::string> split(std::string str, char delim){
 	return tokens;
 }
 
+std::string url_encode(const std::string &value) {
+    std::ostringstream escaped;
+    escaped.fill('0');
+    escaped << std::hex;
+
+    for (std::string::const_iterator i = value.begin(), n = value.end(); i != n; ++i) {
+        std::string::value_type c = (*i);
+
+        // Keep alphanumeric and other accepted characters intact
+        if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
+            escaped << c;
+            continue;
+        }
+
+        // Any other characters are percent-encoded
+        escaped << std::uppercase;
+        escaped << '%' << std::setw(2) << int((unsigned char) c);
+        escaped << std::nouppercase;
+    }
+
+    return escaped.str();
+}
 
 } // anonymous namespace
 
@@ -336,7 +359,7 @@ void PostgreSQL::nodeIsOnline(const uint64_t networkId, const uint64_t memberId,
 	}
 }
 
-std::string PostgreSQL::getSSOAuthURL(const nlohmann::json &member, const std::string &redirectURL)
+AuthInfo PostgreSQL::getSSOAuthInfo(const nlohmann::json &member, const std::string &redirectURL)
 {
 	// NONCE is just a random character string.  no semantic meaning
 	// state = HMAC SHA384 of Nonce based on shared sso key
@@ -347,10 +370,12 @@ std::string PostgreSQL::getSSOAuthURL(const nlohmann::json &member, const std::s
 	// how do we tell when a nonce is used? if auth_expiration_time is set
 	std::string networkId = member["nwid"];
 	std::string memberId = member["id"];
-	char authenticationURL[4096] = {0};
 
-	//fprintf(stderr, "PostgreSQL::updateMemberOnLoad: %s-%s\n", networkId.c_str(), memberId.c_str());
-	bool have_auth = false;
+
+	char authenticationURL[4096] = {0};
+	AuthInfo info;
+	info.enabled = true;
+	// fprintf(stderr, "PostgreSQL::updateMemberOnLoad: %s-%s\n", networkId.c_str(), memberId.c_str());
 	try {
 		auto c = _pool->borrow();
 		pqxx::work w(*c->c);
@@ -390,38 +415,61 @@ std::string PostgreSQL::getSSOAuthURL(const nlohmann::json &member, const std::s
 				exit(6);
 			}
 
-			r = w.exec_params("SELECT org.client_id, org.authorization_endpoint "
+			r = w.exec_params("SELECT org.client_id, org.authorization_endpoint, org.issuer, org.sso_impl_version "
 				"FROM ztc_network AS nw, ztc_org AS org "
 				"WHERE nw.id = $1 AND nw.sso_enabled = true AND org.owner_id = nw.owner_id", networkId);
 		
 			std::string client_id = "";
 			std::string authorization_endpoint = "";
+			std::string issuer = "";
+			uint64_t sso_version = 0;
 
 			if (r.size() == 1) {
 				client_id = r.at(0)[0].as<std::string>();
 				authorization_endpoint = r.at(0)[1].as<std::string>();
+				issuer = r.at(0)[2].as<std::string>();
+				sso_version = r.at(0)[3].as<uint64_t>();
 			} else if (r.size() > 1) {
 				fprintf(stderr, "ERROR: More than one auth endpoint for an organization?!?!? NetworkID: %s\n", networkId.c_str());
 			} else {
 				fprintf(stderr, "No client or auth endpoint?!?\n");
 			}
-
+		
+			info.version = sso_version;
+			
 			// no catch all else because we don't actually care if no records exist here. just continue as normal.
 			if ((!client_id.empty())&&(!authorization_endpoint.empty())) {
-				have_auth = true;
-
+				
 				uint8_t state[48];
 				HMACSHA384(_ssoPsk, nonceBytes, sizeof(nonceBytes), state);
 				char state_hex[256];
 				Utils::hex(state, 48, state_hex);
 				
-				OSUtils::ztsnprintf(authenticationURL, sizeof(authenticationURL),
-					"%s?response_type=id_token&response_mode=form_post&scope=openid+email+profile&redirect_uri=%s&nonce=%s&state=%s&client_id=%s",
-					authorization_endpoint.c_str(),
-					redirectURL.c_str(),
-					nonce.c_str(),
-					state_hex,
-					client_id.c_str());
+				if (info.version == 0) {
+					char url[2048] = {0};
+					OSUtils::ztsnprintf(url, sizeof(authenticationURL),
+						"%s?response_type=id_token&response_mode=form_post&scope=openid+email+profile&redirect_uri=%s&nonce=%s&state=%s&client_id=%s",
+						authorization_endpoint.c_str(),
+						url_encode(redirectURL).c_str(),
+						nonce.c_str(),
+						state_hex,
+						client_id.c_str());
+					info.authenticationURL = std::string(url);
+				} else if (info.version == 1) {
+					info.ssoClientID = client_id;
+					info.issuerURL = issuer;
+					info.ssoNonce = nonce;
+					info.ssoState = std::string(state_hex) + "_" +networkId;
+					info.centralAuthURL = redirectURL;
+					fprintf(
+						stderr,
+						"ssoClientID: %s\nissuerURL: %s\nssoNonce: %s\nssoState: %s\ncentralAuthURL: %s\n",
+						info.ssoClientID.c_str(),
+						info.issuerURL.c_str(),
+						info.ssoNonce.c_str(),
+						info.ssoState.c_str(),
+						info.centralAuthURL.c_str());
+				}
 			}  else {
 				fprintf(stderr, "client_id: %s\nauthorization_endpoint: %s\n", client_id.c_str(), authorization_endpoint.c_str());
 			}
@@ -432,7 +480,7 @@ std::string PostgreSQL::getSSOAuthURL(const nlohmann::json &member, const std::s
 		fprintf(stderr, "ERROR: Error updating member on load: %s\n", e.what());
 	}
 
-	return std::string(authenticationURL);
+	return info; //std::string(authenticationURL);
 }
 
 void PostgreSQL::initializeNetworks()
@@ -1025,23 +1073,47 @@ void PostgreSQL::commitThread()
 			fprintf(stderr, "not an object\n");
 			continue;
 		}
+
+		std::shared_ptr<PostgresConnection> c;
+		try {
+			c = _pool->borrow();
+		} catch (std::exception &e) {
+			fprintf(stderr, "ERROR: %s\n", e.what());
+			continue;
+		}
+
+		if (!c) {
+			fprintf(stderr, "Error getting database connection\n");
+			continue;
+		}
 		
 		try {
-			nlohmann::json *config = &(qitem.first);
-			const std::string objtype = (*config)["objtype"];
+			nlohmann::json &config = (qitem.first);
+			const std::string objtype = config["objtype"];
 			if (objtype == "member") {
 				// fprintf(stderr, "%s: commitThread: member\n", _myAddressStr.c_str());
+				std::string memberId;
+				std::string networkId;
 				try {
-					auto c = _pool->borrow();
 					pqxx::work w(*c->c);
 
-					std::string memberId = (*config)["id"];
-					std::string networkId = (*config)["nwid"];
+					memberId = config["id"];
+					networkId = config["nwid"];
+					
 					std::string target = "NULL";
-					if (!(*config)["remoteTraceTarget"].is_null()) {
-						target = (*config)["remoteTraceTarget"];
+					if (!config["remoteTraceTarget"].is_null()) {
+						target = config["remoteTraceTarget"];
 					}
 					
+					pqxx::row nwrow = w.exec_params1("SELECT COUNT(id) FROM ztc_network WHERE id = $1", networkId);
+					int nwcount = nwrow[0].as<int>();
+
+					if (nwcount != 1) {
+						fprintf(stderr, "network %s does not exist.  skipping member upsert\n", networkId.c_str());
+						w.abort();
+						_pool->unborrow(c);
+						continue;
+					}
 
 					pqxx::result res = w.exec_params0(
 						"INSERT INTO ztc_member (id, network_id, active_bridge, authorized, capabilities, "
@@ -1058,21 +1130,21 @@ void PostgreSQL::commitThread()
 						"v_minor = EXCLUDED.v_minor, v_rev = EXCLUDED.v_rev, v_proto = EXCLUDED.v_proto",
 						memberId,
 						networkId,
-						(bool)(*config)["activeBridge"],
-						(bool)(*config)["authorized"],
-						OSUtils::jsonDump((*config)["capabilities"], -1),
-						OSUtils::jsonString((*config)["identity"], ""),
-						(uint64_t)(*config)["lastAuthorizedTime"],
-						(uint64_t)(*config)["lastDeauthorizedTime"],
-						(bool)(*config)["noAutoAssignIps"],
-						(int)(*config)["remoteTraceLevel"],
+						(bool)config["activeBridge"],
+						(bool)config["authorized"],
+						OSUtils::jsonDump(config["capabilities"], -1),
+						OSUtils::jsonString(config["identity"], ""),
+						(uint64_t)config["lastAuthorizedTime"],
+						(uint64_t)config["lastDeauthorizedTime"],
+						(bool)config["noAutoAssignIps"],
+						(int)config["remoteTraceLevel"],
 						target,
-						(uint64_t)(*config)["revision"],
-						OSUtils::jsonDump((*config)["tags"], -1),
-						(int)(*config)["vMajor"],
-						(int)(*config)["vMinor"],
-						(int)(*config)["vRev"],
-						(int)(*config)["vProto"]);
+						(uint64_t)config["revision"],
+						OSUtils::jsonDump(config["tags"], -1),
+						(int)config["vMajor"],
+						(int)config["vMinor"],
+						(int)config["vRev"],
+						(int)config["vProto"]);
 
 
 					res = w.exec_params0("DELETE FROM ztc_member_ip_assignment WHERE member_id = $1 AND network_id = $2",
@@ -1080,7 +1152,7 @@ void PostgreSQL::commitThread()
 
 					std::vector<std::string> assignments;
 					bool ipAssignError = false;
-					for (auto i = (*config)["ipAssignments"].begin(); i != (*config)["ipAssignments"].end(); ++i) {
+					for (auto i = config["ipAssignments"].begin(); i != config["ipAssignments"].end(); ++i) {
 						std::string addr = *i;
 
 						if (std::find(assignments.begin(), assignments.end(), addr) != assignments.end()) {
@@ -1095,21 +1167,21 @@ void PostgreSQL::commitThread()
 					}
 					if (ipAssignError) {
 						fprintf(stderr, "%s: ipAssignError\n", _myAddressStr.c_str());
-						delete config;
-						config = nullptr;
+						w.abort();
+						_pool->unborrow(c);
+						c.reset();
 						continue;
 					}
 
 					w.commit();
-					_pool->unborrow(c);
 
-					const uint64_t nwidInt = OSUtils::jsonIntHex((*config)["nwid"], 0ULL);
-					const uint64_t memberidInt = OSUtils::jsonIntHex((*config)["id"], 0ULL);
+					const uint64_t nwidInt = OSUtils::jsonIntHex(config["nwid"], 0ULL);
+					const uint64_t memberidInt = OSUtils::jsonIntHex(config["id"], 0ULL);
 					if (nwidInt && memberidInt) {
 						nlohmann::json nwOrig;
 						nlohmann::json memOrig;
 
-						nlohmann::json memNew(*config);
+						nlohmann::json memNew(config);
 
 						get(nwidInt, nwOrig, memberidInt, memOrig);
 
@@ -1117,24 +1189,22 @@ void PostgreSQL::commitThread()
 					} else {
 						fprintf(stderr, "%s: Can't notify of change.  Error parsing nwid or memberid: %llu-%llu\n", _myAddressStr.c_str(), (unsigned long long)nwidInt, (unsigned long long)memberidInt);
 					}
-
 				} catch (std::exception &e) {
-					fprintf(stderr, "%s ERROR: Error updating member: %s\n", _myAddressStr.c_str(), e.what());
+					fprintf(stderr, "%s ERROR: Error updating member %s-%s: %s\n", _myAddressStr.c_str(), networkId.c_str(), memberId.c_str(), e.what());
 				}
 			} else if (objtype == "network") {
 				try {
 					// fprintf(stderr, "%s: commitThread: network\n", _myAddressStr.c_str());
-					auto c = _pool->borrow();
 					pqxx::work w(*c->c);
 
-					std::string id = (*config)["id"];
+					std::string id = config["id"];
 					std::string remoteTraceTarget = "";
-					if(!(*config)["remoteTraceTarget"].is_null()) {
-						remoteTraceTarget = (*config)["remoteTraceTarget"];
+					if(!config["remoteTraceTarget"].is_null()) {
+						remoteTraceTarget = config["remoteTraceTarget"];
 					}
 					std::string rulesSource = "";
-					if ((*config)["rulesSource"].is_string()) {
-						rulesSource = (*config)["rulesSource"];
+					if (config["rulesSource"].is_string()) {
+						rulesSource = config["rulesSource"];
 					}
 
 					// This ugly query exists because when we want to mirror networks to/from
@@ -1163,25 +1233,25 @@ void PostgreSQL::commitThread()
 						"sso_enabled = EXCLUDED.sso_enabled",
 						id,
 						_myAddressStr,
-						OSUtils::jsonDump((*config)["capabilitles"], -1),
-						(bool)(*config)["enableBroadcast"],
+						OSUtils::jsonDump(config["capabilitles"], -1),
+						(bool)config["enableBroadcast"],
 						OSUtils::now(),
-						(int)(*config)["mtu"],
-						(int)(*config)["multicastLimit"],
-						OSUtils::jsonString((*config)["name"],""),
-						(bool)(*config)["private"],
-						(int)(*config)["remoteTraceLevel"],
+						(int)config["mtu"],
+						(int)config["multicastLimit"],
+						OSUtils::jsonString(config["name"],""),
+						(bool)config["private"],
+						(int)config["remoteTraceLevel"],
 						remoteTraceTarget,
-						OSUtils::jsonDump((*config)["rules"], -1),
+						OSUtils::jsonDump(config["rules"], -1),
 						rulesSource,
-						OSUtils::jsonDump((*config)["tags"], -1),
-						OSUtils::jsonDump((*config)["v4AssignMode"],-1),
-						OSUtils::jsonDump((*config)["v6AssignMode"], -1),
-						OSUtils::jsonBool((*config)["ssoEnabled"], false));
+						OSUtils::jsonDump(config["tags"], -1),
+						OSUtils::jsonDump(config["v4AssignMode"],-1),
+						OSUtils::jsonDump(config["v6AssignMode"], -1),
+						OSUtils::jsonBool(config["ssoEnabled"], false));
 
 					res = w.exec_params0("DELETE FROM ztc_network_assignment_pool WHERE network_id = $1", 0);
 
-					auto pool = (*config)["ipAssignmentPools"];
+					auto pool = config["ipAssignmentPools"];
 					bool err = false;
 					for (auto i = pool.begin(); i != pool.end(); ++i) {
 						std::string start = (*i)["ipRangeStart"];
@@ -1194,7 +1264,7 @@ void PostgreSQL::commitThread()
 
 					res = w.exec_params0("DELETE FROM ztc_network_route WHERE network_id = $1", id);
 
-					auto routes = (*config)["routes"];
+					auto routes = config["routes"];
 					err = false;
 					for (auto i = routes.begin(); i != routes.end(); ++i) {
 						std::string t = (*i)["target"];
@@ -1221,12 +1291,10 @@ void PostgreSQL::commitThread()
 						fprintf(stderr, "%s: route add error\n", _myAddressStr.c_str());
 						w.abort();
 						_pool->unborrow(c);
-						delete config;
-						config = nullptr;
 						continue;
 					}
 
-					auto dns = (*config)["dns"];
+					auto dns = config["dns"];
 					std::string domain = dns["domain"];
 					std::stringstream servers;
 					servers << "{";
@@ -1244,12 +1312,11 @@ void PostgreSQL::commitThread()
 						id, domain, s);
 
 					w.commit();
-					_pool->unborrow(c);
 
-					const uint64_t nwidInt = OSUtils::jsonIntHex((*config)["nwid"], 0ULL);
+					const uint64_t nwidInt = OSUtils::jsonIntHex(config["nwid"], 0ULL);
 					if (nwidInt) {
 						nlohmann::json nwOrig;
-						nlohmann::json nwNew(*config);
+						nlohmann::json nwNew(config);
 
 						get(nwidInt, nwOrig);
 
@@ -1257,23 +1324,20 @@ void PostgreSQL::commitThread()
 					} else {
 						fprintf(stderr, "%s: Can't notify network changed: %llu\n", _myAddressStr.c_str(), (unsigned long long)nwidInt);
 					}
-
 				} catch (std::exception &e) {
 					fprintf(stderr, "%s ERROR: Error updating network: %s\n", _myAddressStr.c_str(), e.what());
 				}
 			} else if (objtype == "_delete_network") {
 				// fprintf(stderr, "%s: commitThread: delete network\n", _myAddressStr.c_str());
 				try {
-					auto c = _pool->borrow();
 					pqxx::work w(*c->c);
 
-					std::string networkId = (*config)["nwid"];
+					std::string networkId = config["nwid"];
 
 					pqxx::result res = w.exec_params0("UPDATE ztc_network SET deleted = true WHERE id = $1",
 						networkId);
 
 					w.commit();
-					_pool->unborrow(c);
 				} catch (std::exception &e) {
 					fprintf(stderr, "%s ERROR: Error deleting network: %s\n", _myAddressStr.c_str(), e.what());
 				}
@@ -1281,18 +1345,16 @@ void PostgreSQL::commitThread()
 			} else if (objtype == "_delete_member") {
 				// fprintf(stderr, "%s commitThread: delete member\n", _myAddressStr.c_str());
 				try {
-					auto c = _pool->borrow();
 					pqxx::work w(*c->c);
 
-					std::string memberId = (*config)["id"];
-					std::string networkId = (*config)["nwid"];
+					std::string memberId = config["id"];
+					std::string networkId = config["nwid"];
 
 					pqxx::result res = w.exec_params0(
 						"UPDATE ztc_member SET hidden = true, deleted = true WHERE id = $1 AND network_id = $2",
 						memberId, networkId);
 
 					w.commit();
-					_pool->unborrow(c);
 				} catch (std::exception &e) {
 					fprintf(stderr, "%s ERROR: Error deleting member: %s\n", _myAddressStr.c_str(), e.what());
 				}
@@ -1302,6 +1364,8 @@ void PostgreSQL::commitThread()
 		} catch (std::exception &e) {
 			fprintf(stderr, "%s ERROR: Error getting objtype: %s\n", _myAddressStr.c_str(), e.what());
 		}
+		_pool->unborrow(c);
+		c.reset();
 		std::this_thread::sleep_for(std::chrono::milliseconds(100));
 	}
 
@@ -1321,6 +1385,7 @@ void PostgreSQL::onlineNotification_Postgres()
 	nlohmann::json jtmp1, jtmp2;
 	while (_run == 1) {
 		auto c = _pool->borrow();
+		auto c2 = _pool->borrow();
 		try {
 			fprintf(stderr, "%s onlineNotification_Postgres\n", _myAddressStr.c_str());
 			std::unordered_map< std::pair<uint64_t,uint64_t>,std::pair<int64_t,InetAddress>,_PairHasher > lastOnline;
@@ -1330,15 +1395,16 @@ void PostgreSQL::onlineNotification_Postgres()
 			}
 			
 			pqxx::work w(*c->c);
+			pqxx::work w2(*c2->c);
 
-			// using pqxx::stream_to would be a really nice alternative here, but
-			// unfortunately it doesn't support upserts.
-			// fprintf(stderr, "online notification tick\n");
-			std::stringstream memberUpdate;
-			memberUpdate << "INSERT INTO ztc_member_status (network_id, member_id, address, last_updated) VALUES ";
+			fprintf(stderr, "online notification tick\n");
+			
 			bool firstRun = true;
 			bool memberAdded = false;
 			int updateCount = 0;
+
+			pqxx::pipeline pipe(w);
+
 			for (auto i=lastOnline.begin(); i != lastOnline.end(); ++i) {
 				updateCount += 1;
 				uint64_t nwid_i = i->first.first;
@@ -1355,16 +1421,10 @@ void PostgreSQL::onlineNotification_Postgres()
 				std::string networkId(nwidTmp);
 				std::string memberId(memTmp);
 
-				const char *qvals[2] = {
-					networkId.c_str(),
-					memberId.c_str()
-				};
-
 				try {
-					pqxx::row r = w.exec_params1("SELECT id, network_id FROM ztc_member WHERE network_id = $1 AND id = $2",
+					pqxx::row r = w2.exec_params1("SELECT id, network_id FROM ztc_member WHERE network_id = $1 AND id = $2",
 						networkId, memberId);		
 				} catch (pqxx::unexpected_rows &e) {
-					// fprintf(stderr, "Member count failed: %s\n", e.what());
 					continue;
 				}
 
@@ -1372,32 +1432,30 @@ void PostgreSQL::onlineNotification_Postgres()
 				std::string ipAddr = i->second.second.toIpString(ipTmp);
 				std::string timestamp = std::to_string(ts);
 
-				if (firstRun) {
-					firstRun = false;
-				} else {
-					memberUpdate << ", ";
-				}
-
-				memberUpdate << "('" << networkId << "', '" << memberId << "', ";
+				std::stringstream memberUpdate;
+				memberUpdate << "INSERT INTO ztc_member_status (network_id, member_id, address, last_updated) VALUES "
+					<< "('" << networkId << "', '" << memberId << "', ";
 				if (ipAddr.empty()) {
 					memberUpdate << "NULL, ";
 				} else {
 					memberUpdate << "'" << ipAddr << "', ";
 				}
-				memberUpdate << "TO_TIMESTAMP(" << timestamp << "::double precision/1000))";
-				memberAdded = true;
-			}
-			memberUpdate << " ON CONFLICT (network_id, member_id) DO UPDATE SET address = EXCLUDED.address, last_updated = EXCLUDED.last_updated;";
+				memberUpdate << "TO_TIMESTAMP(" << timestamp << "::double precision/1000)) "
+					<< " ON CONFLICT (network_id, member_id) DO UPDATE SET address = EXCLUDED.address, last_updated = EXCLUDED.last_updated";
 
-			if (memberAdded) {
-				//fprintf(stderr, "%s\n", memberUpdate.str().c_str());
-				pqxx::result res = w.exec0(memberUpdate.str());
-				w.commit();
+				pipe.insert(memberUpdate.str());
+			}
+			while(!pipe.empty()) {
+				pipe.retrieve();
 			}
+
+			pipe.complete();
+			w.commit();
 			fprintf(stderr, "%s: Updated online status of %d members\n", _myAddressStr.c_str(), updateCount);
 		} catch (std::exception &e) {
 			fprintf(stderr, "%s: error in onlinenotification thread: %s\n", _myAddressStr.c_str(), e.what());
 		} 
+		_pool->unborrow(c2);
 		_pool->unborrow(c);
 
 		ConnectionPoolStats stats = _pool->get_stats();

+ 1 - 1
controller/PostgreSQL.hpp

@@ -107,7 +107,7 @@ public:
 	virtual void eraseNetwork(const uint64_t networkId);
 	virtual void eraseMember(const uint64_t networkId, const uint64_t memberId);
 	virtual void nodeIsOnline(const uint64_t networkId, const uint64_t memberId, const InetAddress &physicalAddress);
-	virtual std::string getSSOAuthURL(const nlohmann::json &member, const std::string &redirectURL);
+	virtual AuthInfo getSSOAuthInfo(const nlohmann::json &member, const std::string &redirectURL);
 
 protected:
 	struct _PairHasher

+ 1 - 1
ext/central-controller-docker/Dockerfile

@@ -2,7 +2,7 @@
 FROM registry.zerotier.com/zerotier/controller-builder:latest as builder
 MAINTAINER Adam Ierymekno <[email protected]>, Grant Limberg <[email protected]>
 ADD . /ZeroTierOne
-RUN cd ZeroTierOne && make clean && make central-controller -j8
+RUN export PATH=$PATH:~/.cargo/bin && cd ZeroTierOne && make clean && make central-controller -j8
 
 FROM registry.zerotier.com/zerotier/controller-run:latest
 COPY --from=builder /ZeroTierOne/zerotier-one /usr/local/bin/zerotier-one

+ 2 - 1
ext/central-controller-docker/Dockerfile.builder

@@ -9,4 +9,5 @@ RUN yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x
 RUN dnf -qy module disable postgresql
 RUN yum -y install epel-release && yum -y update && yum clean all
 RUN yum groupinstall -y "Development Tools" && yum clean all
-RUN yum install -y bash cmake postgresql10 postgresql10-devel clang jemalloc jemalloc-devel libpqxx libpqxx-devel && yum clean all
+RUN yum install -y bash cmake postgresql10 postgresql10-devel clang jemalloc jemalloc-devel libpqxx libpqxx-devel openssl-devel && yum clean all
+RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y

+ 1 - 1
ext/central-controller-docker/Dockerfile.run_base

@@ -2,4 +2,4 @@ FROM centos:8
 RUN yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm
 RUN dnf -qy module disable postgresql 
 RUN yum -y install epel-release && yum -y update && yum clean all
-RUN yum install -y jemalloc jemalloc-devel postgresql10 libpqxx && yum clean all
+RUN yum install -y jemalloc jemalloc-devel postgresql10 libpqxx libpqxx-devel && yum clean all

+ 32 - 0
include/ZeroTierOne.h

@@ -1194,11 +1194,18 @@ typedef struct
 	 */
 	ZT_VirtualNetworkDNS dns;
 
+
+
 	/**
 	 * sso enabled
 	 */
 	bool ssoEnabled;
 
+	/**
+	 * SSO verison
+	 */
+	uint64_t ssoVersion;
+
 	/**
 	 * If the status us AUTHENTICATION_REQUIRED, this may contain a URL for authentication.
 	 */
@@ -1208,6 +1215,31 @@ typedef struct
 	 * Time that current authentication expires. only valid if ssoEnabled is true
 	 */
 	uint64_t authenticationExpiryTime;
+
+	/**
+	 * OIDC issuer URL.
+	 */
+	char issuerURL[2048];
+
+	/**
+	 * central base URL.
+	 */
+	char centralAuthURL[2048];
+
+	/**
+	 * sso nonce
+	 */
+	char ssoNonce[128];
+
+	/**
+	 * sso state
+	 */
+	char ssoState[256];
+
+	/**
+	 * oidc client id
+	 */
+	char ssoClientID[256];
 } ZT_VirtualNetworkConfig;
 
 /**

+ 17 - 4
make-linux.mk

@@ -9,7 +9,7 @@ ifeq ($(origin CXX),default)
         CXX:=$(shell if [ -e /opt/rh/devtoolset-8/root/usr/bin/g++ ]; then echo /opt/rh/devtoolset-8/root/usr/bin/g++; else echo $(CXX); fi)
 endif
 
-INCLUDES?=
+INCLUDES?=-Izeroidc/target
 DEFS?=
 LDLIBS?=
 DESTDIR?=
@@ -18,7 +18,7 @@ include objects.mk
 ONE_OBJS+=osdep/LinuxEthernetTap.o
 ONE_OBJS+=osdep/LinuxNetLink.o
 
-# for central controller builds
+# for central controller buildsk
 TIMESTAMP=$(shell date +"%Y%m%d%H%M")
 
 # Auto-detect miniupnpc and nat-pmp as well and use system libs if present,
@@ -41,6 +41,12 @@ else
 	override DEFS+=-DZT_USE_SYSTEM_NATPMP
 endif
 
+ifeq ($(ZT_DEBUG),1)
+	LDLIBS+=zeroidc/target/debug/libzeroidc.a -ldl
+else
+	LDLIBS+=zeroidc/target/release/libzeroidc.a -ldl
+endif
+
 # Use bundled http-parser since distribution versions are NOT API-stable or compatible!
 # Trying to use dynamically linked libhttp-parser causes tons of compatibility problems.
 ONE_OBJS+=ext/http-parser/http_parser.o
@@ -64,6 +70,7 @@ ifeq ($(ZT_DEBUG),1)
 	override CFLAGS+=-Wall -Wno-deprecated -g -O -pthread $(INCLUDES) $(DEFS)
 	override CXXFLAGS+=-Wall -Wno-deprecated -g -O -std=c++11 -pthread $(INCLUDES) $(DEFS)
 	ZT_TRACE=1
+	RUSTFLAGS=
 	# The following line enables optimization for the crypto code, since
 	# C25519 in particular is almost UNUSABLE in -O0 even on a 3ghz box!
 node/Salsa20.o node/SHA512.o node/C25519.o node/Poly1305.o: CXXFLAGS=-Wall -O2 -g -pthread $(INCLUDES) $(DEFS)
@@ -73,6 +80,7 @@ else
 	CXXFLAGS?=-O3 -fstack-protector -fPIE
 	override CXXFLAGS+=-Wall -Wno-deprecated -std=c++11 -pthread $(INCLUDES) -DNDEBUG $(DEFS)
 	LDFLAGS=-pie -Wl,-z,relro,-z,now
+	RUSTFLAGS=--release
 endif
 
 ifeq ($(ZT_QNAP), 1)
@@ -274,7 +282,7 @@ endif
 
 ifeq ($(ZT_CONTROLLER),1)
 	override CXXFLAGS+=-Wall -Wno-deprecated -std=c++17 -pthread $(INCLUDES) -DNDEBUG $(DEFS)
-	override LDLIBS+=-L/usr/pgsql-10/lib/ -lpqxx -lpq ext/hiredis-0.14.1/lib/centos8/libhiredis.a ext/redis-plus-plus-1.1.1/install/centos8/lib/libredis++.a
+	override LDLIBS+=-L/usr/pgsql-10/lib/ -lpqxx -lpq ext/hiredis-0.14.1/lib/centos8/libhiredis.a ext/redis-plus-plus-1.1.1/install/centos8/lib/libredis++.a -lssl -lcrypto
 	override DEFS+=-DZT_CONTROLLER_USE_LIBPQ
 	override INCLUDES+=-I/usr/pgsql-10/include -Iext/hiredis-0.14.1/include/ -Iext/redis-plus-plus-1.1.1/install/centos8/include/sw/
 endif
@@ -321,6 +329,8 @@ zerotier-idtool: zerotier-one
 zerotier-cli: zerotier-one
 	ln -sf zerotier-one zerotier-cli
 
+$(ONE_OBJS): zeroidc
+
 libzerotiercore.a:	FORCE
 	make CFLAGS="-O3 -fstack-protector -fPIC" CXXFLAGS="-O3 -std=c++11 -fstack-protector -fPIC" $(CORE_OBJS)
 	ar rcs libzerotiercore.a $(CORE_OBJS)
@@ -339,7 +349,7 @@ manpages:	FORCE
 doc:	manpages
 
 clean: FORCE
-	rm -rf *.a *.so *.o node/*.o controller/*.o osdep/*.o service/*.o ext/http-parser/*.o ext/miniupnpc/*.o ext/libnatpmp/*.o $(CORE_OBJS) $(ONE_OBJS) zerotier-one zerotier-idtool zerotier-cli zerotier-selftest build-* ZeroTierOneInstaller-* *.deb *.rpm .depend debian/files debian/zerotier-one*.debhelper debian/zerotier-one.substvars debian/*.log debian/zerotier-one doc/node_modules ext/misc/*.o debian/.debhelper debian/debhelper-build-stamp docker/zerotier-one
+	rm -rf *.a *.so *.o node/*.o controller/*.o osdep/*.o service/*.o ext/http-parser/*.o ext/miniupnpc/*.o ext/libnatpmp/*.o $(CORE_OBJS) $(ONE_OBJS) zerotier-one zerotier-idtool zerotier-cli zerotier-selftest build-* ZeroTierOneInstaller-* *.deb *.rpm .depend debian/files debian/zerotier-one*.debhelper debian/zerotier-one.substvars debian/*.log debian/zerotier-one doc/node_modules ext/misc/*.o debian/.debhelper debian/debhelper-build-stamp docker/zerotier-one zeroidc/target
 
 distclean:	clean
 
@@ -361,6 +371,9 @@ debug:	FORCE
 	make ZT_DEBUG=1 one
 	make ZT_DEBUG=1 selftest
 
+zeroidc:	FORCE
+	cd zeroidc && cargo build $(RUSTFLAGS)
+
 # Note: keep the symlinks in /var/lib/zerotier-one to the binaries since these
 # provide backward compatibility with old releases where the binaries actually
 # lived here. Folks got scripts.

+ 21 - 6
make-mac.mk

@@ -1,6 +1,8 @@
 CC=clang
 CXX=clang++
-INCLUDES=
+TOPDIR=$(shell PWD)
+
+INCLUDES=-I$(shell PWD)/zeroidc/target
 DEFS=
 LIBS=
 ARCH_FLAGS=-arch x86_64 -arch arm64 
@@ -26,7 +28,7 @@ DEFS+=-DZT_BUILD_PLATFORM=$(ZT_BUILD_PLATFORM) -DZT_BUILD_ARCHITECTURE=$(ZT_BUIL
 
 include objects.mk
 ONE_OBJS+=osdep/MacEthernetTap.o osdep/MacKextEthernetTap.o osdep/MacDNSHelper.o ext/http-parser/http_parser.o
-LIBS+=-framework CoreServices -framework SystemConfiguration -framework CoreFoundation
+LIBS+=-framework CoreServices -framework SystemConfiguration -framework CoreFoundation -framework Security
 
 # Official releases are signed with our Apple cert and apply software updates by default
 ifeq ($(ZT_OFFICIAL_RELEASE),1)
@@ -73,6 +75,8 @@ ifeq ($(ZT_DEBUG),1)
 	ARCH_FLAGS=
 	CFLAGS+=-Wall -g $(INCLUDES) $(DEFS) $(ARCH_FLAGS)
 	STRIP=echo
+	RUSTFLAGS=
+	RUST_VARIANT=debug
 	# The following line enables optimization for the crypto code, since
 	# C25519 in particular is almost UNUSABLE in heavy testing without it.
 node/Salsa20.o node/SHA512.o node/C25519.o node/Poly1305.o: CFLAGS = -Wall -O2 -g $(INCLUDES) $(DEFS)
@@ -80,6 +84,8 @@ else
 	CFLAGS?=-Ofast -fstack-protector-strong
 	CFLAGS+=$(ARCH_FLAGS) -Wall -flto -fPIE -mmacosx-version-min=$(MACOS_VERSION_MIN) -DNDEBUG -Wno-unused-private-field $(INCLUDES) $(DEFS)
 	STRIP=strip
+	RUSTFLAGS=--release
+	RUST_VARIANT=release
 endif
 
 ifeq ($(ZT_TRACE),1)
@@ -103,8 +109,8 @@ mac-agent: FORCE
 osdep/MacDNSHelper.o: osdep/MacDNSHelper.mm
 	$(CXX) $(CXXFLAGS) -c osdep/MacDNSHelper.mm -o osdep/MacDNSHelper.o 
 
-one:	$(CORE_OBJS) $(ONE_OBJS) one.o mac-agent
-	$(CXX) $(CXXFLAGS) -o zerotier-one $(CORE_OBJS) $(ONE_OBJS) one.o $(LIBS)
+one:	zeroidc $(CORE_OBJS) $(ONE_OBJS) one.o mac-agent 
+	$(CXX) $(CXXFLAGS) -o zerotier-one $(CORE_OBJS) $(ONE_OBJS) one.o $(LIBS) zeroidc/target/libzeroidc.a
 	# $(STRIP) zerotier-one
 	ln -sf zerotier-one zerotier-idtool
 	ln -sf zerotier-one zerotier-cli
@@ -112,6 +118,13 @@ one:	$(CORE_OBJS) $(ONE_OBJS) one.o mac-agent
 
 zerotier-one: one
 
+zeroidc: zeroidc/target/libzeroidc.a
+
+zeroidc/target/libzeroidc.a:	FORCE
+	cd zeroidc && MACOSX_DEPLOYMENT_TARGET=$(MACOS_VERSION_MIN) cargo build --target=x86_64-apple-darwin $(RUSTFLAGS)
+	cd zeroidc && MACOSX_DEPLOYMENT_TARGET=$(MACOS_VERSION_MIN) cargo build --target=aarch64-apple-darwin $(RUSTFLAGS)
+	cd zeroidc && lipo -create target/x86_64-apple-darwin/$(RUST_VARIANT)/libzeroidc.a target/aarch64-apple-darwin/$(RUST_VARIANT)/libzeroidc.a -output target/libzeroidc.a
+
 central-controller:
 	make ARCH_FLAGS="-arch x86_64" ZT_CONTROLLER=1 one
 
@@ -119,6 +132,8 @@ zerotier-idtool: one
 
 zerotier-cli: one
 
+$(ONE_OBJS): zeroidc
+
 libzerotiercore.a:	$(CORE_OBJS)
 	ar rcs libzerotiercore.a $(CORE_OBJS)
 	ranlib libzerotiercore.a
@@ -130,7 +145,7 @@ core: libzerotiercore.a
 #	$(STRIP) zerotier
 
 selftest: $(CORE_OBJS) $(ONE_OBJS) selftest.o
-	$(CXX) $(CXXFLAGS) -o zerotier-selftest selftest.o $(CORE_OBJS) $(ONE_OBJS) $(LIBS)
+	$(CXX) $(CXXFLAGS) -o zerotier-selftest selftest.o $(CORE_OBJS) $(ONE_OBJS) $(LIBS) zeroidc/target/libzeroidc.a
 	$(STRIP) zerotier-selftest
 
 zerotier-selftest: selftest
@@ -157,7 +172,7 @@ central-controller-docker: FORCE
 	docker build --no-cache -t registry.zerotier.com/zerotier-central/ztcentral-controller:${TIMESTAMP} -f ext/central-controller-docker/Dockerfile --build-arg git_branch=$(shell git name-rev --name-only HEAD) .
 
 clean:
-	rm -rf MacEthernetTapAgent *.dSYM build-* *.a *.pkg *.dmg *.o node/*.o controller/*.o service/*.o osdep/*.o ext/http-parser/*.o $(CORE_OBJS) $(ONE_OBJS) zerotier-one zerotier-idtool zerotier-selftest zerotier-cli zerotier doc/node_modules zt1_update_$(ZT_BUILD_PLATFORM)_$(ZT_BUILD_ARCHITECTURE)_*
+	rm -rf MacEthernetTapAgent *.dSYM build-* *.a *.pkg *.dmg *.o node/*.o controller/*.o service/*.o osdep/*.o ext/http-parser/*.o $(CORE_OBJS) $(ONE_OBJS) zerotier-one zerotier-idtool zerotier-selftest zerotier-cli zerotier doc/node_modules zt1_update_$(ZT_BUILD_PLATFORM)_$(ZT_BUILD_ARCHITECTURE)_* zeroidc/target/
 
 distclean:	clean
 

+ 42 - 12
node/IncomingPacket.cpp

@@ -142,7 +142,7 @@ bool IncomingPacket::_doERROR(const RuntimeEnvironment *RR,void *tPtr,const Shar
 			if (inReVerb == Packet::VERB_NETWORK_CONFIG_REQUEST) {
 				const SharedPtr<Network> network(RR->node->network(at<uint64_t>(ZT_PROTO_VERB_ERROR_IDX_PAYLOAD)));
 				if ((network)&&(network->controller() == peer->address()))
-					network->setNotFound();
+					network->setNotFound(tPtr);
 			}
 			break;
 
@@ -153,7 +153,7 @@ bool IncomingPacket::_doERROR(const RuntimeEnvironment *RR,void *tPtr,const Shar
 			if (inReVerb == Packet::VERB_NETWORK_CONFIG_REQUEST) {
 				const SharedPtr<Network> network(RR->node->network(at<uint64_t>(ZT_PROTO_VERB_ERROR_IDX_PAYLOAD)));
 				if ((network)&&(network->controller() == peer->address()))
-					network->setNotFound();
+					network->setNotFound(tPtr);
 			}
 			break;
 
@@ -176,7 +176,7 @@ bool IncomingPacket::_doERROR(const RuntimeEnvironment *RR,void *tPtr,const Shar
 			// Network controller: network access denied.
 			const SharedPtr<Network> network(RR->node->network(at<uint64_t>(ZT_PROTO_VERB_ERROR_IDX_PAYLOAD)));
 			if ((network)&&(network->controller() == peer->address()))
-				network->setAccessDenied();
+				network->setAccessDenied(tPtr);
 		}	break;
 
 		case Packet::ERROR_UNWANTED_MULTICAST: {
@@ -191,25 +191,55 @@ bool IncomingPacket::_doERROR(const RuntimeEnvironment *RR,void *tPtr,const Shar
 		}	break;
 
 		case Packet::ERROR_NETWORK_AUTHENTICATION_REQUIRED: {
+			fprintf(stderr, "\nPacket::ERROR_NETWORK_AUTHENTICATION_REQUIRED\n\n");
 			const SharedPtr<Network> network(RR->node->network(at<uint64_t>(ZT_PROTO_VERB_ERROR_IDX_PAYLOAD)));
 			if ((network)&&(network->controller() == peer->address())) {
-				bool noUrl = true;
 				int s = (int)size() - (ZT_PROTO_VERB_ERROR_IDX_PAYLOAD + 8);
 				if (s > 2) {
 					const uint16_t errorDataSize = at<uint16_t>(ZT_PROTO_VERB_ERROR_IDX_PAYLOAD + 8);
 					s -= 2;
 					if (s >= (int)errorDataSize) {
-						Dictionary<3072> authInfo(((const char *)this->data()) + (ZT_PROTO_VERB_ERROR_IDX_PAYLOAD + 10), errorDataSize);
-						char authenticationURL[2048];
-						if (authInfo.get("aU", authenticationURL, sizeof(authenticationURL)) > 0) {
-							authenticationURL[sizeof(authenticationURL) - 1] = 0; // ensure always zero terminated
-							network->setAuthenticationRequired(authenticationURL);
-							noUrl = false;
+						Dictionary<8192> authInfo(((const char *)this->data()) + (ZT_PROTO_VERB_ERROR_IDX_PAYLOAD + 10), errorDataSize);
+
+						uint64_t authVer = authInfo.getUI(ZT_AUTHINFO_DICT_KEY_VERSION, 0ULL);
+
+						if (authVer == 0) {
+							char authenticationURL[2048];
+
+							if (authInfo.get(ZT_AUTHINFO_DICT_KEY_AUTHENTICATION_URL, authenticationURL, sizeof(authenticationURL)) > 0) {
+								authenticationURL[sizeof(authenticationURL) - 1] = 0; // ensure always zero terminated
+								network->setAuthenticationRequired(tPtr, authenticationURL);
+							}
+						} else if (authVer == 1) {
+							char issuerURL[2048] = { 0 };
+							char centralAuthURL[2048] = { 0 };
+							char ssoNonce[64] = { 0 };
+							char ssoState[128] = {0};
+							char ssoClientID[256] = { 0 };
+
+							if (authInfo.get(ZT_AUTHINFO_DICT_KEY_ISSUER_URL, issuerURL, sizeof(issuerURL)) > 0) {
+								issuerURL[sizeof(issuerURL) - 1] = 0;
+							}
+							if (authInfo.get(ZT_AUTHINFO_DICT_KEY_CENTRAL_ENDPOINT_URL, centralAuthURL, sizeof(centralAuthURL))>0) {
+								centralAuthURL[sizeof(centralAuthURL) - 1] = 0;
+							}
+							if (authInfo.get(ZT_AUTHINFO_DICT_KEY_NONCE, ssoNonce, sizeof(ssoNonce)) > 0) {
+								ssoNonce[sizeof(ssoNonce) - 1] = 0;
+							}
+							if (authInfo.get(ZT_AUTHINFO_DICT_KEY_STATE, ssoState, sizeof(ssoState)) > 0) {
+								ssoState[sizeof(ssoState) - 1] = 0;
+							}
+							if (authInfo.get(ZT_AUTHINFO_DICT_KEY_CLIENT_ID, ssoClientID, sizeof(ssoClientID)) > 0) {
+								ssoClientID[sizeof(ssoClientID) - 1] = 0;
+							}
+
+							network->setAuthenticationRequired(tPtr, issuerURL, centralAuthURL, ssoClientID, ssoNonce, ssoState);
 						}
 					}
+				} else {
+					fprintf(stderr, "authinfo??????\n");
+					network->setAuthenticationRequired(tPtr, "");
 				}
-				if (noUrl)
-					network->setAuthenticationRequired("");
 			}
 		}	break;
 

+ 29 - 2
node/Network.cpp

@@ -1115,7 +1115,7 @@ void Network::requestConfiguration(void *tPtr)
 				this->setConfiguration(tPtr,*nconf,false);
 				delete nconf;
 			} else {
-				this->setNotFound();
+				this->setNotFound(tPtr);
 			}
 		} else if ((_id & 0xff) == 0x01) {
 			// ffAAaaaaaaaaaa01 -- where AA is the IPv4 /8 to use and aaaaaaaaaa is the anchor node for multicast gather and replication
@@ -1199,7 +1199,7 @@ void Network::requestConfiguration(void *tPtr)
 		if (RR->localNetworkController) {
 			RR->localNetworkController->request(_id,InetAddress(),0xffffffffffffffffULL,RR->identity,rmd);
 		} else {
-			this->setNotFound();
+			this->setNotFound(tPtr);
 		}
 		return;
 	}
@@ -1434,8 +1434,14 @@ void Network::_externalConfig(ZT_VirtualNetworkConfig *ec) const
 	memcpy(&ec->dns, &_config.dns, sizeof(ZT_VirtualNetworkDNS));
 
 	Utils::scopy(ec->authenticationURL, sizeof(ec->authenticationURL), _authenticationURL.c_str());
+	ec->ssoVersion = _config.ssoVersion;
 	ec->authenticationExpiryTime = _config.authenticationExpiryTime;
 	ec->ssoEnabled = _config.ssoEnabled;
+	Utils::scopy(ec->centralAuthURL, sizeof(ec->centralAuthURL), _config.centralAuthURL);
+	Utils::scopy(ec->issuerURL, sizeof(ec->issuerURL), _config.issuerURL);
+	Utils::scopy(ec->ssoNonce, sizeof(ec->ssoNonce), _config.ssoNonce);
+	Utils::scopy(ec->ssoState, sizeof(ec->ssoState), _config.ssoState);
+	Utils::scopy(ec->ssoClientID, sizeof(ec->ssoClientID), _config.ssoClientID);
 }
 
 void Network::_sendUpdatesToMembers(void *tPtr,const MulticastGroup *const newMulticastGroup)
@@ -1542,4 +1548,25 @@ Membership &Network::_membership(const Address &a)
 	return _memberships[a];
 }
 
+void Network::setAuthenticationRequired(void *tPtr, const char* issuerURL, const char* centralEndpoint, const char* clientID, const char* nonce, const char* state)
+{
+	Mutex::Lock _l(_lock);
+	_netconfFailure = NETCONF_FAILURE_AUTHENTICATION_REQUIRED;
+	_config.ssoEnabled = true;
+	_config.ssoVersion = 1;
+
+	Utils::scopy(_config.issuerURL, sizeof(_config.issuerURL), issuerURL);
+	Utils::scopy(_config.centralAuthURL, sizeof(_config.centralAuthURL), centralEndpoint);
+	Utils::scopy(_config.ssoClientID, sizeof(_config.ssoClientID), clientID);
+	Utils::scopy(_config.ssoNonce, sizeof(_config.ssoNonce), nonce);
+	Utils::scopy(_config.ssoState, sizeof(_config.ssoState), state);
+	_sendUpdateEvent(tPtr);
+}
+
+void Network::_sendUpdateEvent(void *tPtr) {
+	ZT_VirtualNetworkConfig ctmp;
+	_externalConfig(&ctmp);
+	RR->node->configureVirtualNetworkPort(tPtr, _id, &_uPtr, (_portInitialized) ? ZT_VIRTUAL_NETWORK_CONFIG_OPERATION_CONFIG_UPDATE : ZT_VIRTUAL_NETWORK_CONFIG_OPERATION_UP, &ctmp);
+}
+
 } // namespace ZeroTier

+ 18 - 5
node/Network.hpp

@@ -205,31 +205,43 @@ public:
 	/**
 	 * Set netconf failure to 'access denied' -- called in IncomingPacket when controller reports this
 	 */
-	inline void setAccessDenied()
+	inline void setAccessDenied(void *tPtr)
 	{
 		Mutex::Lock _l(_lock);
 		_netconfFailure = NETCONF_FAILURE_ACCESS_DENIED;
+
+		_sendUpdateEvent(tPtr);
 	}
 
 	/**
 	 * Set netconf failure to 'not found' -- called by IncomingPacket when controller reports this
 	 */
-	inline void setNotFound()
+	inline void setNotFound(void *tPtr)
 	{
 		Mutex::Lock _l(_lock);
 		_netconfFailure = NETCONF_FAILURE_NOT_FOUND;
+
+		_sendUpdateEvent(tPtr);
 	}
 
 	/**
 	 * Set netconf failure to 'authentication required' possibly with an authorization URL
 	 */
-	inline void setAuthenticationRequired(const char *url)
+	inline void setAuthenticationRequired(void *tPtr, const char *url)
 	{
 		Mutex::Lock _l(_lock);
 		_netconfFailure = NETCONF_FAILURE_AUTHENTICATION_REQUIRED;
 		_authenticationURL = (url) ? url : "";
 		_config.ssoEnabled = true;
-	}	
+		_config.ssoVersion = 0;
+		_sendUpdateEvent(tPtr);
+	}
+
+	/**
+	 * set netconf failure to 'authentication required' along with info needed
+	 * for sso full flow authentication.
+	 */
+	void setAuthenticationRequired(void *tPtr, const char* issuerURL, const char* centralEndpoint, const char* clientID, const char* nonce, const char* state);
 
 	/**
 	 * Causes this network to request an updated configuration from its master node now
@@ -413,6 +425,7 @@ private:
 	void _announceMulticastGroupsTo(void *tPtr,const Address &peer,const std::vector<MulticastGroup> &allMulticastGroups);
 	std::vector<MulticastGroup> _allMulticastGroups() const;
 	Membership &_membership(const Address &a);
+	void _sendUpdateEvent(void *tPtr);
 
 	const RuntimeEnvironment *const RR;
 	void *_uPtr;
@@ -459,6 +472,6 @@ private:
 	AtomicCounter __refCount;
 };
 
-} // namespace ZeroTier
+}	// namespace ZeroTier
 
 #endif

+ 60 - 13
node/NetworkConfig.cpp

@@ -182,12 +182,25 @@ bool NetworkConfig::toDictionary(Dictionary<ZT_NETWORKCONFIG_DICT_CAPACITY> &d,b
 			if (!d.add(ZT_NETWORKCONFIG_DICT_KEY_DNS,*tmp)) return false;
 		}
 
-		if (!d.add(ZT_NETWORKCONFIG_DICT_KEY_SSO_ENABLED, this->ssoEnabled)) return false;
-		if (this->ssoEnabled) {
-			if (this->authenticationURL[0]) {
-				if (!d.add(ZT_NETWORKCONFIG_DICT_KEY_AUTHENTICATION_URL, this->authenticationURL)) return false;
+		if (this->ssoVersion == 0) {
+			if (!d.add(ZT_NETWORKCONFIG_DICT_KEY_SSO_VERSION, this->ssoVersion)) return false;
+			if (!d.add(ZT_NETWORKCONFIG_DICT_KEY_SSO_ENABLED, this->ssoEnabled)) return false;
+
+			if (this->ssoEnabled) {
+				if (this->authenticationURL[0]) {
+					if (!d.add(ZT_NETWORKCONFIG_DICT_KEY_AUTHENTICATION_URL, this->authenticationURL)) return false;
+				}
+				if (!d.add(ZT_NETWORKCONFIG_DICT_KEY_AUTHENTICATION_EXPIRY_TIME, this->authenticationExpiryTime)) return false;
 			}
-			if (!d.add(ZT_NETWORKCONFIG_DICT_KEY_AUTHENTICATION_EXPIRY_TIME, this->authenticationExpiryTime)) return false;
+		} else if(this->ssoVersion == 1) {
+			if (!d.add(ZT_NETWORKCONFIG_DICT_KEY_SSO_VERSION, this->ssoVersion)) return false;
+			if (!d.add(ZT_NETWORKCONFIG_DICT_KEY_SSO_ENABLED, this->ssoEnabled)) return false;
+			//if (!d.add(ZT_NETWORKCONFIG_DICT_KEY_AUTHENTICATION_URL, this->authenticationURL)) return false;
+			if (!d.add(ZT_NETWORKCONFIG_DICT_KEY_ISSUER_URL, this->issuerURL)) return false;
+			if (!d.add(ZT_NETWORKCONFIG_DICT_KEY_CENTRAL_ENDPOINT_URL, this->centralAuthURL)) return false;
+			if (!d.add(ZT_NETWORKCONFIG_DICT_KEY_NONCE, this->ssoNonce)) return false;
+			if (!d.add(ZT_NETWORKCONFIG_DICT_KEY_STATE, this->ssoState)) return false;
+			if (!d.add(ZT_NETWORKCONFIG_DICT_KEY_CLIENT_ID, this->ssoClientID)) return false;
 		}
 
 		delete tmp;
@@ -374,18 +387,52 @@ bool NetworkConfig::fromDictionary(const Dictionary<ZT_NETWORKCONFIG_DICT_CAPACI
 				DNS::deserializeDNS(*tmp, p, &dns);
 			}
 
-
+			this->ssoVersion = d.getUI(ZT_NETWORKCONFIG_DICT_KEY_SSO_VERSION, 0ULL);
 			this->ssoEnabled = d.getB(ZT_NETWORKCONFIG_DICT_KEY_SSO_ENABLED, false);
-			if (this->ssoEnabled) {
-				if (d.get(ZT_NETWORKCONFIG_DICT_KEY_AUTHENTICATION_URL, this->authenticationURL, (unsigned int)sizeof(this->authenticationURL)) > 0) {
-					this->authenticationURL[sizeof(this->authenticationURL) - 1] = 0; // ensure null terminated
+
+			if (this->ssoVersion == 0) {
+				// implicit flow
+				if (this->ssoEnabled) {
+					if (d.get(ZT_NETWORKCONFIG_DICT_KEY_AUTHENTICATION_URL, this->authenticationURL, (unsigned int)sizeof(this->authenticationURL)) > 0) {
+						this->authenticationURL[sizeof(this->authenticationURL) - 1] = 0; // ensure null terminated
+					} else {
+						this->authenticationURL[0] = 0;
+					}
+					this->authenticationExpiryTime = d.getI(ZT_NETWORKCONFIG_DICT_KEY_AUTHENTICATION_EXPIRY_TIME, 0);
 				} else {
 					this->authenticationURL[0] = 0;
+					this->authenticationExpiryTime = 0;
+				}
+			} else if (this->ssoVersion == 1) {
+				// full flow
+				if (this->ssoEnabled) {
+					if (d.get(ZT_NETWORKCONFIG_DICT_KEY_AUTHENTICATION_URL, this->authenticationURL, (unsigned int)sizeof(this->authenticationURL)) > 0) {
+						this->authenticationURL[sizeof(this->authenticationURL) - 1] = 0;
+					}
+					if (d.get(ZT_NETWORKCONFIG_DICT_KEY_ISSUER_URL, this->issuerURL, (unsigned int)sizeof(this->issuerURL)) > 0) {
+						this->issuerURL[sizeof(this->issuerURL) - 1] = 0;
+					}
+					if (d.get(ZT_NETWORKCONFIG_DICT_KEY_CENTRAL_ENDPOINT_URL, this->centralAuthURL, (unsigned int)sizeof(this->centralAuthURL)) > 0) {
+						this->centralAuthURL[sizeof(this->centralAuthURL) - 1] = 0;
+					}
+					if (d.get(ZT_NETWORKCONFIG_DICT_KEY_NONCE, this->ssoNonce, (unsigned int)sizeof(this->ssoNonce)) > 0) {
+						this->ssoNonce[sizeof(this->ssoNonce) - 1] = 0;
+					}
+					if (d.get(ZT_NETWORKCONFIG_DICT_KEY_STATE, this->ssoState, (unsigned int)sizeof(this->ssoState)) > 0) {
+						this->ssoState[sizeof(this->ssoState) - 1] = 0;
+					}
+					if (d.get(ZT_NETWORKCONFIG_DICT_KEY_CLIENT_ID, this->ssoClientID, (unsigned int)sizeof(this->ssoClientID)) > 0) {
+						this->ssoClientID[sizeof(this->ssoClientID) - 1] = 0;
+					}
+				} else {
+					this->authenticationURL[0] = 0;
+					this->authenticationExpiryTime = 0;
+					this->centralAuthURL[0] = 0;
+					this->ssoNonce[0] = 0;
+					this->ssoState[0] = 0;
+					this->ssoClientID[0] = 0;
+					this->issuerURL[0] = 0;
 				}
-				this->authenticationExpiryTime = d.getI(ZT_NETWORKCONFIG_DICT_KEY_AUTHENTICATION_EXPIRY_TIME, 0);
-			} else {
-				this->authenticationURL[0] = 0;
-				this->authenticationExpiryTime = 0;
 			}
 		}
 

+ 74 - 2
node/NetworkConfig.hpp

@@ -94,7 +94,7 @@
 namespace ZeroTier {
 
 // Dictionary capacity needed for max size network config
-#define ZT_NETWORKCONFIG_DICT_CAPACITY (4096 + (sizeof(ZT_VirtualNetworkRule) * ZT_MAX_NETWORK_RULES) + (sizeof(Capability) * ZT_MAX_NETWORK_CAPABILITIES) + (sizeof(Tag) * ZT_MAX_NETWORK_TAGS) + (sizeof(CertificateOfOwnership) * ZT_MAX_CERTIFICATES_OF_OWNERSHIP))
+#define ZT_NETWORKCONFIG_DICT_CAPACITY (4096 + (sizeof(ZT_VirtualNetworkConfig)) + (sizeof(ZT_VirtualNetworkRule) * ZT_MAX_NETWORK_RULES) + (sizeof(Capability) * ZT_MAX_NETWORK_CAPABILITIES) + (sizeof(Tag) * ZT_MAX_NETWORK_TAGS) + (sizeof(CertificateOfOwnership) * ZT_MAX_CERTIFICATES_OF_OWNERSHIP))
 
 // Dictionary capacity needed for max size network meta-data
 #define ZT_NETWORKCONFIG_METADATA_DICT_CAPACITY 1024
@@ -180,10 +180,39 @@ namespace ZeroTier {
 #define ZT_NETWORKCONFIG_DICT_KEY_DNS "DNS"
 // sso enabld
 #define ZT_NETWORKCONFIG_DICT_KEY_SSO_ENABLED "ssoe"
+// so version
+#define ZT_NETWORKCONFIG_DICT_KEY_SSO_VERSION "ssov"
 // authentication URL
 #define ZT_NETWORKCONFIG_DICT_KEY_AUTHENTICATION_URL "aurl"
 // authentication expiry
 #define ZT_NETWORKCONFIG_DICT_KEY_AUTHENTICATION_EXPIRY_TIME "aexpt"
+// oidc issuer URL
+#define ZT_NETWORKCONFIG_DICT_KEY_ISSUER_URL "iurl"
+// central endpoint
+#define ZT_NETWORKCONFIG_DICT_KEY_CENTRAL_ENDPOINT_URL "ssoce"
+// nonce
+#define ZT_NETWORKCONFIG_DICT_KEY_NONCE "sson"
+// state
+#define ZT_NETWORKCONFIG_DICT_KEY_STATE "ssos"
+// client ID
+#define ZT_NETWORKCONFIG_DICT_KEY_CLIENT_ID "ssocid"
+
+// AuthInfo fields -- used by ncSendError for sso
+
+// AuthInfo Version
+#define ZT_AUTHINFO_DICT_KEY_VERSION "aV"
+// authenticaiton URL
+#define ZT_AUTHINFO_DICT_KEY_AUTHENTICATION_URL "aU"
+// issuer URL
+#define ZT_AUTHINFO_DICT_KEY_ISSUER_URL "iU"
+// Central endpoint URL
+#define ZT_AUTHINFO_DICT_KEY_CENTRAL_ENDPOINT_URL "aCU"
+// Nonce
+#define ZT_AUTHINFO_DICT_KEY_NONCE "aN"
+// State
+#define ZT_AUTHINFO_DICT_KEY_STATE "aS"
+// Client ID
+#define ZT_AUTHINFO_DICT_KEY_CLIENT_ID "aCID"
 
 // Legacy fields -- these are obsoleted but are included when older clients query
 
@@ -242,7 +271,12 @@ public:
 		dnsCount(0),
 		ssoEnabled(false),
 		authenticationURL(),
-		authenticationExpiryTime(0)
+		authenticationExpiryTime(0),
+		issuerURL(),
+		centralAuthURL(),
+		ssoNonce(),
+		ssoState(),
+		ssoClientID()
 	{
 		name[0] = 0;
 		memset(specialists, 0, sizeof(uint64_t)*ZT_MAX_NETWORK_SPECIALISTS);
@@ -250,6 +284,12 @@ public:
 		memset(staticIps, 0, sizeof(InetAddress)*ZT_MAX_ZT_ASSIGNED_ADDRESSES);
 		memset(rules, 0, sizeof(ZT_VirtualNetworkRule)*ZT_MAX_NETWORK_RULES);
 		memset(&dns, 0, sizeof(ZT_VirtualNetworkDNS));
+		memset(authenticationURL, 0, sizeof(authenticationURL));
+		memset(issuerURL, 0, sizeof(issuerURL));
+		memset(centralAuthURL, 0, sizeof(centralAuthURL));
+		memset(ssoNonce, 0, sizeof(ssoNonce));
+		memset(ssoState, 0, sizeof(ssoState));
+		memset(ssoClientID, 0, sizeof(ssoClientID));
 	}
 
 	/**
@@ -619,6 +659,11 @@ public:
 	 */
 	bool ssoEnabled;
 
+	/**
+	 * SSO verison
+	 */
+	uint64_t ssoVersion;
+
 	/**
 	 * Authentication URL if authentication is required
 	 */
@@ -626,8 +671,35 @@ public:
 
 	/**
 	 * Time current authentication expires or 0 if external authentication is disabled
+	 * 
+	 * Not used if authVersion >= 1
 	 */
 	uint64_t authenticationExpiryTime;
+
+	/**
+	 * OIDC issuer URL
+	 */
+	char issuerURL[2048];
+
+	/**
+	 * central base URL.
+	 */
+	char centralAuthURL[2048];
+
+	/**
+	 * sso nonce
+	 */
+	char ssoNonce[128];
+
+	/**
+	 * sso state
+	 */
+	char ssoState[256];
+
+	/**
+	 * oidc client id
+	 */
+	char ssoClientID[256];
 };
 
 } // namespace ZeroTier

+ 5 - 3
node/Node.cpp

@@ -735,14 +735,16 @@ void Node::ncSendError(uint64_t nwid,uint64_t requestPacketId,const Address &des
 		switch(errorCode) {
 			case NetworkController::NC_ERROR_OBJECT_NOT_FOUND:
 			case NetworkController::NC_ERROR_INTERNAL_SERVER_ERROR:
-				n->setNotFound();
+				n->setNotFound(nullptr);
 				break;
 			case NetworkController::NC_ERROR_ACCESS_DENIED:
-				n->setAccessDenied();
+				n->setAccessDenied(nullptr);
 				break;
 			case NetworkController::NC_ERROR_AUTHENTICATION_REQUIRED: {
-			}
+				fprintf(stderr, "\n\nGot auth required\n\n");
+
 				break;
+			} 
 
 			default: break;
 		}

+ 423 - 187
service/OneService.cpp

@@ -53,6 +53,8 @@
 #include "OneService.hpp"
 #include "SoftwareUpdater.hpp"
 
+#include <zeroidc.h>
+
 #ifdef __WINDOWS__
 #include <winsock2.h>
 #include <windows.h>
@@ -145,6 +147,205 @@ size_t curlResponseWrite(void *ptr, size_t size, size_t nmemb, std::string *data
 
 namespace ZeroTier {
 
+// Configured networks
+class NetworkState
+{
+public:
+	NetworkState() 
+		: _webPort(9993)
+		, _tap((EthernetTap *)0)
+		, _idc(nullptr)
+	{
+		// Real defaults are in network 'up' code in network event handler
+		_settings.allowManaged = true;
+		_settings.allowGlobal = false;
+		_settings.allowDefault = false;
+		_settings.allowDNS = false;
+		memset(&_config, 0, sizeof(ZT_VirtualNetworkConfig));
+	}
+
+	~NetworkState()
+	{
+		this->_managedRoutes.clear();
+		this->_tap.reset();
+
+		if (_idc) {
+			zeroidc::zeroidc_stop(_idc);
+			zeroidc::zeroidc_delete(_idc);
+			_idc = nullptr;
+		}
+	}
+
+	void setWebPort(unsigned int port) {
+		_webPort = port;
+	}
+
+	void setTap(std::shared_ptr<EthernetTap> tap) {
+		this->_tap = tap;
+	}
+
+	std::shared_ptr<EthernetTap> tap() const {
+		return _tap;
+	}
+
+	OneService::NetworkSettings settings() const {
+		return _settings;
+	}
+
+	void setSettings(const OneService::NetworkSettings &settings) {
+		_settings = settings;
+	}
+
+	void setAllowManaged(bool allow) {
+		_settings.allowManaged = allow;
+	}
+
+	bool allowManaged() const {
+		return _settings.allowManaged;
+	}
+
+	void setAllowGlobal(bool allow) {
+		_settings.allowGlobal = allow;
+	}
+
+	bool allowGlobal() const {
+		return _settings.allowGlobal;
+	}
+
+	void setAllowDefault(bool allow) {
+		_settings.allowDefault = allow;
+	}
+
+	bool allowDefault() const {
+		return _settings.allowDefault;
+	}
+
+	void setAllowDNS(bool allow) {
+		_settings.allowDNS = allow;
+	}
+
+	bool allowDNS() const {
+		return _settings.allowDNS;
+	}
+	
+	std::vector<InetAddress> allowManagedWhitelist() const {
+		return _settings.allowManagedWhitelist;
+	}
+
+	void addToAllowManagedWhiteList(const InetAddress& addr) {
+		_settings.allowManagedWhitelist.push_back(addr);
+	}
+
+	const ZT_VirtualNetworkConfig& config() {
+		return _config;
+	}
+
+	void setConfig(const ZT_VirtualNetworkConfig *nwc) {
+		char nwbuf[17] = {};
+		const char* nwid = Utils::hex(nwc->nwid, nwbuf);
+		// fprintf(stderr, "NetworkState::setConfig(%s)\n", nwid);
+
+		memcpy(&_config, nwc, sizeof(ZT_VirtualNetworkConfig));
+		// fprintf(stderr, "ssoEnabled: %s, ssoVersion: %d\n", 
+		// 	_config.ssoEnabled ? "true" : "false", _config.ssoVersion);
+
+		if (_config.ssoEnabled && _config.ssoVersion == 1) {
+			//  fprintf(stderr, "ssoEnabled for %s\n", nwid);
+			if (_idc == nullptr)
+			{
+				assert(_config.issuerURL != nullptr);
+				assert(_config.ssoClientID != nullptr);
+				assert(_config.centralAuthURL != nullptr);
+
+				// fprintf(stderr, "Issuer URL: %s\n", _config.issuerURL);
+				// fprintf(stderr, "Client ID: %s\n", _config.ssoClientID);
+				// fprintf(stderr, "Central Auth URL: %s\n", _config.centralAuthURL);
+				
+				_idc = zeroidc::zeroidc_new(
+					_config.issuerURL,
+					_config.ssoClientID,
+					_config.centralAuthURL,
+					_webPort
+				);
+
+				if (_idc == nullptr) {
+					fprintf(stderr, "idc is null\n");
+					return;
+				}
+
+				// fprintf(stderr, "idc created (%s, %s, %s)\n", _config.issuerURL, _config.ssoClientID, _config.centralAuthURL);
+			}
+
+			zeroidc::zeroidc_set_nonce_and_csrf(
+				_idc,
+				_config.ssoState,
+				_config.ssoNonce
+			);
+
+			const char* url = zeroidc::zeroidc_get_auth_url(_idc);
+			memcpy(_config.authenticationURL, url, strlen(url));
+			_config.authenticationURL[strlen(url)] = 0;
+		}
+	}
+
+	std::vector<InetAddress>& managedIps()  {
+		return _managedIps;
+	}
+
+	void setManagedIps(const std::vector<InetAddress> &managedIps) {
+		_managedIps = managedIps;
+	}
+
+	std::map< InetAddress, SharedPtr<ManagedRoute> >& managedRoutes() {
+		return _managedRoutes;
+	}
+
+	const char* getAuthURL() {
+		if (_idc != nullptr) {
+			return zeroidc::zeroidc_get_auth_url(_idc);
+		}
+		fprintf(stderr, "_idc is null\n");
+		return "";
+	}
+
+	const char* doTokenExchange(const char *code) {
+		if (_idc == nullptr) {
+			fprintf(stderr, "ainfo or idc null\n");
+			return "";
+		}
+
+		const char *ret = zeroidc::zeroidc_token_exchange(_idc, code);
+		zeroidc::zeroidc_set_nonce_and_csrf(
+			_idc,
+			_config.ssoState,
+			_config.ssoNonce
+		);
+
+		const char* url = zeroidc::zeroidc_get_auth_url(_idc);
+		memcpy(_config.authenticationURL, url, strlen(url));
+		_config.authenticationURL[strlen(url)] = 0;
+		return ret;
+	}
+
+	uint64_t getExpiryTime() {
+		if (_idc == nullptr) {
+			fprintf(stderr, "idc is null\n");
+			return 0;
+		}
+
+		return zeroidc::zeroidc_get_exp_time(_idc);
+	}
+
+private:
+	unsigned int _webPort;
+	std::shared_ptr<EthernetTap> _tap;
+	ZT_VirtualNetworkConfig _config; // memcpy() of raw config from core
+	std::vector<InetAddress> _managedIps;
+	std::map< InetAddress, SharedPtr<ManagedRoute> > _managedRoutes;
+	OneService::NetworkSettings _settings;
+	zeroidc::ZeroIDC *_idc;
+};
+
 namespace {
 
 static const InetAddress NULL_INET_ADDR;
@@ -171,12 +372,12 @@ static std::string _trimString(const std::string &s)
 	return s.substr(start,end - start);
 }
 
-static void _networkToJson(nlohmann::json &nj,const ZT_VirtualNetworkConfig *nc,const std::string &portDeviceName,const OneService::NetworkSettings &localSettings)
+static void _networkToJson(nlohmann::json &nj,NetworkState &ns)
 {
 	char tmp[256];
 
 	const char *nstatus = "",*ntype = "";
-	switch(nc->status) {
+	switch(ns.config().status) {
 		case ZT_NETWORK_STATUS_REQUESTING_CONFIGURATION: nstatus = "REQUESTING_CONFIGURATION"; break;
 		case ZT_NETWORK_STATUS_OK:                       nstatus = "OK"; break;
 		case ZT_NETWORK_STATUS_ACCESS_DENIED:            nstatus = "ACCESS_DENIED"; break;
@@ -185,75 +386,81 @@ static void _networkToJson(nlohmann::json &nj,const ZT_VirtualNetworkConfig *nc,
 		case ZT_NETWORK_STATUS_CLIENT_TOO_OLD:           nstatus = "CLIENT_TOO_OLD"; break;
 		case ZT_NETWORK_STATUS_AUTHENTICATION_REQUIRED:  nstatus = "AUTHENTICATION_REQUIRED"; break;
 	}
-	switch(nc->type) {
+	switch(ns.config().type) {
 		case ZT_NETWORK_TYPE_PRIVATE:                    ntype = "PRIVATE"; break;
 		case ZT_NETWORK_TYPE_PUBLIC:                     ntype = "PUBLIC"; break;
 	}
 
-	OSUtils::ztsnprintf(tmp,sizeof(tmp),"%.16llx",nc->nwid);
+	OSUtils::ztsnprintf(tmp,sizeof(tmp),"%.16llx",ns.config().nwid);
 	nj["id"] = tmp;
 	nj["nwid"] = tmp;
-	OSUtils::ztsnprintf(tmp,sizeof(tmp),"%.2x:%.2x:%.2x:%.2x:%.2x:%.2x",(unsigned int)((nc->mac >> 40) & 0xff),(unsigned int)((nc->mac >> 32) & 0xff),(unsigned int)((nc->mac >> 24) & 0xff),(unsigned int)((nc->mac >> 16) & 0xff),(unsigned int)((nc->mac >> 8) & 0xff),(unsigned int)(nc->mac & 0xff));
+	OSUtils::ztsnprintf(tmp,sizeof(tmp),"%.2x:%.2x:%.2x:%.2x:%.2x:%.2x",(unsigned int)((ns.config().mac >> 40) & 0xff),(unsigned int)((ns.config().mac >> 32) & 0xff),(unsigned int)((ns.config().mac >> 24) & 0xff),(unsigned int)((ns.config().mac >> 16) & 0xff),(unsigned int)((ns.config().mac >> 8) & 0xff),(unsigned int)(ns.config().mac & 0xff));
 	nj["mac"] = tmp;
-	nj["name"] = nc->name;
+	nj["name"] = ns.config().name;
 	nj["status"] = nstatus;
 	nj["type"] = ntype;
-	nj["mtu"] = nc->mtu;
-	nj["dhcp"] = (bool)(nc->dhcp != 0);
-	nj["bridge"] = (bool)(nc->bridge != 0);
-	nj["broadcastEnabled"] = (bool)(nc->broadcastEnabled != 0);
-	nj["portError"] = nc->portError;
-	nj["netconfRevision"] = nc->netconfRevision;
-	nj["portDeviceName"] = portDeviceName;
+	nj["mtu"] = ns.config().mtu;
+	nj["dhcp"] = (bool)(ns.config().dhcp != 0);
+	nj["bridge"] = (bool)(ns.config().bridge != 0);
+	nj["broadcastEnabled"] = (bool)(ns.config().broadcastEnabled != 0);
+	nj["portError"] = ns.config().portError;
+	nj["netconfRevision"] = ns.config().netconfRevision;
+	nj["portDeviceName"] = ns.tap()->deviceName();
+
+	OneService::NetworkSettings localSettings = ns.settings();
+
 	nj["allowManaged"] = localSettings.allowManaged;
 	nj["allowGlobal"] = localSettings.allowGlobal;
 	nj["allowDefault"] = localSettings.allowDefault;
 	nj["allowDNS"] = localSettings.allowDNS;
 
 	nlohmann::json aa = nlohmann::json::array();
-	for(unsigned int i=0;i<nc->assignedAddressCount;++i) {
-		aa.push_back(reinterpret_cast<const InetAddress *>(&(nc->assignedAddresses[i]))->toString(tmp));
+	for(unsigned int i=0;i<ns.config().assignedAddressCount;++i) {
+		aa.push_back(reinterpret_cast<const InetAddress *>(&(ns.config().assignedAddresses[i]))->toString(tmp));
 	}
 	nj["assignedAddresses"] = aa;
 
 	nlohmann::json ra = nlohmann::json::array();
-	for(unsigned int i=0;i<nc->routeCount;++i) {
+	for(unsigned int i=0;i<ns.config().routeCount;++i) {
 		nlohmann::json rj;
-		rj["target"] = reinterpret_cast<const InetAddress *>(&(nc->routes[i].target))->toString(tmp);
-		if (nc->routes[i].via.ss_family == nc->routes[i].target.ss_family)
-			rj["via"] = reinterpret_cast<const InetAddress *>(&(nc->routes[i].via))->toIpString(tmp);
+		rj["target"] = reinterpret_cast<const InetAddress *>(&(ns.config().routes[i].target))->toString(tmp);
+		if (ns.config().routes[i].via.ss_family == ns.config().routes[i].target.ss_family)
+			rj["via"] = reinterpret_cast<const InetAddress *>(&(ns.config().routes[i].via))->toIpString(tmp);
 		else rj["via"] = nlohmann::json();
-		rj["flags"] = (int)nc->routes[i].flags;
-		rj["metric"] = (int)nc->routes[i].metric;
+		rj["flags"] = (int)ns.config().routes[i].flags;
+		rj["metric"] = (int)ns.config().routes[i].metric;
 		ra.push_back(rj);
 	}
 	nj["routes"] = ra;
 
 	nlohmann::json mca = nlohmann::json::array();
-	for(unsigned int i=0;i<nc->multicastSubscriptionCount;++i) {
+	for(unsigned int i=0;i<ns.config().multicastSubscriptionCount;++i) {
 		nlohmann::json m;
-		m["mac"] = MAC(nc->multicastSubscriptions[i].mac).toString(tmp);
-		m["adi"] = nc->multicastSubscriptions[i].adi;
+		m["mac"] = MAC(ns.config().multicastSubscriptions[i].mac).toString(tmp);
+		m["adi"] = ns.config().multicastSubscriptions[i].adi;
 		mca.push_back(m);
 	}
 	nj["multicastSubscriptions"] = mca;
 
 	nlohmann::json m;
-	m["domain"] = nc->dns.domain;
+	m["domain"] = ns.config().dns.domain;
 	m["servers"] = nlohmann::json::array();
 	for(int j=0;j<ZT_MAX_DNS_SERVERS;++j) {
 
-		InetAddress a(nc->dns.server_addr[j]);
+		InetAddress a(ns.config().dns.server_addr[j]);
 		if (a.isV4() || a.isV6()) {
 			char buf[256];
 			m["servers"].push_back(a.toIpString(buf));
 		}
 	}
 	nj["dns"] = m;
-
-	nj["authenticationURL"] = nc->authenticationURL;
-	nj["authenticationExpiryTime"] = nc->authenticationExpiryTime;
-	nj["ssoEnabled"] = nc->ssoEnabled;
+	if (ns.config().ssoEnabled) {
+		const char* authURL = ns.getAuthURL();
+		//fprintf(stderr, "Auth URL: %s\n", authURL);
+		nj["authenticationURL"] = authURL;
+		nj["authenticationExpiryTime"] = (ns.getExpiryTime()*1000);
+		nj["ssoEnabled"] = ns.config().ssoEnabled;
+	}
 }
 
 static void _peerToJson(nlohmann::json &pj,const ZT_Peer *peer)
@@ -525,32 +732,8 @@ public:
 	// Deadline for the next background task service function
 	volatile int64_t _nextBackgroundTaskDeadline;
 
-	// Configured networks
-	struct NetworkState
-	{
-		NetworkState() :
-			tap((EthernetTap *)0)
-		{
-			// Real defaults are in network 'up' code in network event handler
-			settings.allowManaged = true;
-			settings.allowGlobal = false;
-			settings.allowDefault = false;
-			settings.allowDNS = false;
-			memset(&config, 0, sizeof(ZT_VirtualNetworkConfig));
-		}
+	
 
-		~NetworkState()
-		{
-			this->managedRoutes.clear();
-			this->tap.reset();
-		}
-
-		std::shared_ptr<EthernetTap> tap;
-		ZT_VirtualNetworkConfig config; // memcpy() of raw config from core
-		std::vector<InetAddress> managedIps;
-		std::map< InetAddress, SharedPtr<ManagedRoute> > managedRoutes;
-		NetworkSettings settings;
-	};
 	std::map<uint64_t,NetworkState> _nets;
 	Mutex _nets_m;
 
@@ -888,7 +1071,7 @@ public:
 					{
 						Mutex::Lock _l(_nets_m);
 						for(std::map<uint64_t,NetworkState>::iterator n(_nets.begin());n!=_nets.end();++n) {
-							if (n->second.tap)
+							if (n->second.tap())
 								syncManagedStuff(n->second,false,true,false);
 						}
 					}
@@ -913,9 +1096,9 @@ public:
 						Mutex::Lock _l(_nets_m);
 						mgChanges.reserve(_nets.size() + 1);
 						for(std::map<uint64_t,NetworkState>::const_iterator n(_nets.begin());n!=_nets.end();++n) {
-							if (n->second.tap) {
+							if (n->second.tap()) {
 								mgChanges.push_back(std::pair< uint64_t,std::pair< std::vector<MulticastGroup>,std::vector<MulticastGroup> > >(n->first,std::pair< std::vector<MulticastGroup>,std::vector<MulticastGroup> >()));
-								n->second.tap->scanMulticastGroups(mgChanges.back().second.first,mgChanges.back().second.second);
+								n->second.tap()->scanMulticastGroups(mgChanges.back().second.first,mgChanges.back().second.second);
 							}
 						}
 					}
@@ -1114,8 +1297,8 @@ public:
 	{
 		Mutex::Lock _l(_nets_m);
 		std::map<uint64_t,NetworkState>::const_iterator n(_nets.find(nwid));
-		if ((n != _nets.end())&&(n->second.tap))
-			return n->second.tap->deviceName();
+		if ((n != _nets.end())&&(n->second.tap()))
+			return n->second.tap()->deviceName();
 		else return std::string();
 	}
 
@@ -1129,10 +1312,10 @@ public:
 	{
 		Mutex::Lock _l(_nets_m);
 		NetworkState &n = _nets[nwid];
-		*numRoutes = *numRoutes < n.config.routeCount ? *numRoutes : n.config.routeCount;
+		*numRoutes = *numRoutes < n.config().routeCount ? *numRoutes : n.config().routeCount;
 		for(unsigned int i=0; i<*numRoutes; i++) {
 			ZT_VirtualNetworkRoute *vnr = (ZT_VirtualNetworkRoute*)routeArray;
-			memcpy(&vnr[i], &(n.config.routes[i]), sizeof(ZT_VirtualNetworkRoute));
+			memcpy(&vnr[i], &(n.config().routes[i]), sizeof(ZT_VirtualNetworkRoute));
 		}
 	}
 
@@ -1157,32 +1340,23 @@ public:
 		std::map<uint64_t,NetworkState>::const_iterator n(_nets.find(nwid));
 		if (n == _nets.end())
 			return false;
-		settings = n->second.settings;
+		settings = n->second.settings();
 		return true;
 	}
 
 	virtual bool setNetworkSettings(const uint64_t nwid,const NetworkSettings &settings)
 	{
-		Mutex::Lock _l(_nets_m);
-
-		std::map<uint64_t,NetworkState>::iterator n(_nets.find(nwid));
-		if (n == _nets.end())
-			return false;
-		n->second.settings = settings;
-
 		char nlcpath[4096];
 		OSUtils::ztsnprintf(nlcpath,sizeof(nlcpath),"%s" ZT_PATH_SEPARATOR_S "%.16llx.local.conf",_networksPath.c_str(),nwid);
 		FILE *out = fopen(nlcpath,"w");
 		if (out) {
-			fprintf(out,"allowManaged=%d\n",(int)n->second.settings.allowManaged);
-			fprintf(out,"allowGlobal=%d\n",(int)n->second.settings.allowGlobal);
-			fprintf(out,"allowDefault=%d\n",(int)n->second.settings.allowDefault);
-			fprintf(out,"allowDNS=%d\n",(int)n->second.settings.allowDNS);
+			fprintf(out,"allowManaged=%d\n",(int)settings.allowManaged);
+			fprintf(out,"allowGlobal=%d\n",(int)settings.allowGlobal);
+			fprintf(out,"allowDefault=%d\n",(int)settings.allowDefault);
+			fprintf(out,"allowDNS=%d\n",(int)settings.allowDNS);
 			fclose(out);
 		}
 
-		if (n->second.tap)
-			syncManagedStuff(n->second,true,true,true);
 
 		return true;
 	}
@@ -1386,17 +1560,17 @@ public:
 
 					}
 				} else if (ps[0] == "network") {
-					ZT_VirtualNetworkList *nws = _node->networks();
-					if (nws) {
+					Mutex::Lock _l(_nets_m);
+					if (!_nets.empty()) {
 						if (ps.size() == 1) {
 							// Return [array] of all networks
 
 							res = nlohmann::json::array();
-							for(unsigned long i=0;i<nws->networkCount;++i) {
-								OneService::NetworkSettings localSettings;
-								getNetworkSettings(nws->networks[i].nwid,localSettings);
+							
+							for (auto it = _nets.begin(); it != _nets.end(); ++it) {
+								NetworkState &ns = it->second;
 								nlohmann::json nj;
-								_networkToJson(nj,&(nws->networks[i]),portDeviceName(nws->networks[i].nwid),localSettings);
+								_networkToJson(nj, ns);
 								res.push_back(nj);
 							}
 
@@ -1405,19 +1579,20 @@ public:
 							// Return a single network by ID or 404 if not found
 
 							const uint64_t wantnw = Utils::hexStrToU64(ps[1].c_str());
-							for(unsigned long i=0;i<nws->networkCount;++i) {
-								if (nws->networks[i].nwid == wantnw) {
-									OneService::NetworkSettings localSettings;
-									getNetworkSettings(nws->networks[i].nwid,localSettings);
-									_networkToJson(res,&(nws->networks[i]),portDeviceName(nws->networks[i].nwid),localSettings);
-									scode = 200;
-									break;
-								}
+							if (_nets.find(wantnw) != _nets.end()) {
+								res = json::object();
+								NetworkState& ns = _nets[wantnw];
+								_networkToJson(res, ns);
+								scode = 200;
 							}
-
-						} else scode = 404;
-						_node->freeQueryResult((void *)nws);
-					} else scode = 500;
+						} else {
+							fprintf(stderr, "not found\n");
+							scode = 404;
+						}
+					} else {
+						fprintf(stderr, "_nets is empty??\n");
+						scode = 500;
+					}
 				} else if (ps[0] == "peer") {
 					ZT_PeerList *pl = _node->peers();
 					if (pl) {
@@ -1482,7 +1657,63 @@ public:
 					} else scode = 404;
 				}
 
-			} else scode = 401; // isAuth == false
+			} else if (ps[0] == "sso") {
+				// SSO redirect handling
+				const char* state = zeroidc::zeroidc_get_url_param_value("state", path.c_str());
+				const char* nwid = zeroidc::zeroidc_network_id_from_state(state);
+				
+				const uint64_t id = Utils::hexStrToU64(nwid);
+				Mutex::Lock l(_nets_m);
+				if (_nets.find(id) != _nets.end()) {
+					NetworkState& ns = _nets[id];
+					const char* code = zeroidc::zeroidc_get_url_param_value("code", path.c_str());
+					ns.doTokenExchange(code);
+					scode = 200;
+					responseBody = "<html>\
+<head>\
+<style type=\"text/css\">\
+html,body {\
+	background: #eeeeee;\
+	margin: 0;\
+	padding: 0;\
+	font-family: \"Helvetica\";\
+	font-weight: bold;\
+	font-size: 12pt;\
+	height: 100%;\
+	width: 100%;\
+}\
+div.icon {\
+	background: #ffb354;\
+	color: #000000;\
+	font-size: 120pt;\
+	border-radius: 2.5rem;\
+	display: inline-block;\
+	width: 1.3em;\
+	height: 1.3em;\
+	padding: 0;\
+	margin: 15;\
+	line-height: 1.4em;\
+	vertical-align: middle;\
+	text-align: center;\
+}\
+</style>\
+</head>\
+<body>\
+<br><br><br><br><br><br>\
+<center>\
+<div class=\"icon\">&#x23c1;</div>\
+<div class=\"text\">Authentication Successful. You may now access the network.</div>\
+</center>\
+</body>\
+</html>";
+					responseContentType = "text/html";
+					return scode;
+				} else {
+					scode = 404;
+				}
+			} else {
+				scode = 401; // isAuth == false && !sso
+			}
 		} else if ((httpMethod == HTTP_POST)||(httpMethod == HTTP_PUT)) {
  			if (isAuth) {
 				if (ps[0] == "bond") {
@@ -1579,37 +1810,41 @@ public:
 
 						uint64_t wantnw = Utils::hexStrToU64(ps[1].c_str());
 						_node->join(wantnw,(void *)0,(void *)0); // does nothing if we are a member
-						ZT_VirtualNetworkList *nws = _node->networks();
-						if (nws) {
-							for(unsigned long i=0;i<nws->networkCount;++i) {
-								if (nws->networks[i].nwid == wantnw) {
-									OneService::NetworkSettings localSettings;
-									getNetworkSettings(nws->networks[i].nwid,localSettings);
-
-									try {
-										json j(OSUtils::jsonParse(body));
-										if (j.is_object()) {
-											json &allowManaged = j["allowManaged"];
-											if (allowManaged.is_boolean()) localSettings.allowManaged = (bool)allowManaged;
-											json &allowGlobal = j["allowGlobal"];
-											if (allowGlobal.is_boolean()) localSettings.allowGlobal = (bool)allowGlobal;
-											json &allowDefault = j["allowDefault"];
-											if (allowDefault.is_boolean()) localSettings.allowDefault = (bool)allowDefault;
-											json &allowDNS = j["allowDNS"];
-											if (allowDNS.is_boolean()) localSettings.allowDNS = (bool)allowDNS;
-										}
-									} catch ( ... ) {
-										// discard invalid JSON
+						Mutex::Lock l(_nets_m);
+						if (!_nets.empty()) {
+							if (_nets.find(wantnw) != _nets.end()) {
+								NetworkState& ns = _nets[wantnw];
+								try {
+									json j(OSUtils::jsonParse(body));
+								
+									json &allowManaged = j["allowManaged"];
+									if (allowManaged.is_boolean()) {
+										ns.setAllowManaged((bool)allowManaged);
 									}
+									json& allowGlobal = j["allowGlobal"];
+									if (allowGlobal.is_boolean()) {
+										ns.setAllowGlobal((bool)allowGlobal);
+									}
+									json& allowDefault = j["allowDefault"];
+									if (allowDefault.is_boolean()) {
+										ns.setAllowDefault((bool)allowDefault);
+									}
+									json& allowDNS = j["allowDNS"];
+									if (allowDNS.is_boolean()) {
+										ns.setAllowDNS((bool)allowDNS);
+									}
+								} catch (...) {
+									// discard invalid JSON
+								}
+								setNetworkSettings(wantnw, ns.settings());
+								if (ns.tap()) {
+									syncManagedStuff(ns,true,true,true);
+								}
 
-									setNetworkSettings(nws->networks[i].nwid,localSettings);
-									_networkToJson(res,&(nws->networks[i]),portDeviceName(nws->networks[i].nwid),localSettings);
+								_networkToJson(res, ns);
 
-									scode = 200;
-									break;
-								}
+								scode = 200;
 							}
-							_node->freeQueryResult((void *)nws);
 						} else scode = 500;
 
 					} else scode = 404;
@@ -1618,8 +1853,10 @@ public:
 						scode = _controller->handleControlPlaneHttpPOST(std::vector<std::string>(ps.begin()+1,ps.end()),urlArgs,headers,body,responseBody,responseContentType);
 					else scode = 404;
 				}
-
-			} else scode = 401; // isAuth == false
+			}
+			else {
+				scode = 401; // isAuth == false
+			}
 		} else if (httpMethod == HTTP_DELETE) {
 			if (isAuth) {
 
@@ -1650,7 +1887,6 @@ public:
 						scode = _controller->handleControlPlaneHttpDELETE(std::vector<std::string>(ps.begin()+1,ps.end()),urlArgs,headers,body,responseBody,responseContentType);
 					else scode = 404;
 				}
-
 			} else scode = 401; // isAuth = false
 		} else {
 			scode = 400;
@@ -1971,12 +2207,12 @@ public:
 	// Checks if a managed IP or route target is allowed
 	bool checkIfManagedIsAllowed(const NetworkState &n,const InetAddress &target)
 	{
-		if (!n.settings.allowManaged)
+		if (!n.allowManaged())
 			return false;
 
-		if (!n.settings.allowManagedWhitelist.empty()) {
+		if (!n.allowManagedWhitelist().empty()) {
 			bool allowed = false;
-			for (InetAddress addr : n.settings.allowManagedWhitelist) {
+			for (InetAddress addr : n.allowManagedWhitelist()) {
 				if (addr.containsAddress(target) && addr.netmaskBits() <= target.netmaskBits()) {
 					allowed = true;
 					break;
@@ -1986,7 +2222,7 @@ public:
 		}
 
 		if (target.isDefaultRoute())
-			return n.settings.allowDefault;
+			return n.allowDefault();
 		switch(target.ipScope()) {
 			case InetAddress::IP_SCOPE_NONE:
 			case InetAddress::IP_SCOPE_MULTICAST:
@@ -1994,7 +2230,7 @@ public:
 			case InetAddress::IP_SCOPE_LINK_LOCAL:
 				return false;
 			case InetAddress::IP_SCOPE_GLOBAL:
-				return n.settings.allowGlobal;
+				return n.allowGlobal();
 			default:
 				return true;
 		}
@@ -2018,18 +2254,18 @@ public:
 		// assumes _nets_m is locked
 		if (syncIps) {
 			std::vector<InetAddress> newManagedIps;
-			newManagedIps.reserve(n.config.assignedAddressCount);
-			for(unsigned int i=0;i<n.config.assignedAddressCount;++i) {
-				const InetAddress *ii = reinterpret_cast<const InetAddress *>(&(n.config.assignedAddresses[i]));
+			newManagedIps.reserve(n.config().assignedAddressCount);
+			for(unsigned int i=0;i<n.config().assignedAddressCount;++i) {
+				const InetAddress *ii = reinterpret_cast<const InetAddress *>(&(n.config().assignedAddresses[i]));
 				if (checkIfManagedIsAllowed(n,*ii))
 					newManagedIps.push_back(*ii);
 			}
 			std::sort(newManagedIps.begin(),newManagedIps.end());
 			newManagedIps.erase(std::unique(newManagedIps.begin(),newManagedIps.end()),newManagedIps.end());
 
-			for(std::vector<InetAddress>::iterator ip(n.managedIps.begin());ip!=n.managedIps.end();++ip) {
+			for(std::vector<InetAddress>::iterator ip(n.managedIps().begin());ip!=n.managedIps().end();++ip) {
 				if (std::find(newManagedIps.begin(),newManagedIps.end(),*ip) == newManagedIps.end()) {
-					if (!n.tap->removeIp(*ip))
+					if (!n.tap()->removeIp(*ip))
 						fprintf(stderr,"ERROR: unable to remove ip address %s" ZT_EOL_S, ip->toString(ipbuf));
 				}
 			}
@@ -2038,39 +2274,39 @@ public:
 				fprintf(stderr,"ERROR: unable to add ip addresses to ifcfg" ZT_EOL_S);
 #else
 			for(std::vector<InetAddress>::iterator ip(newManagedIps.begin());ip!=newManagedIps.end();++ip) {
-				if (std::find(n.managedIps.begin(),n.managedIps.end(),*ip) == n.managedIps.end()) {
-					if (!n.tap->addIp(*ip))
+				if (std::find(n.managedIps().begin(),n.managedIps().end(),*ip) == n.managedIps().end()) {
+					if (!n.tap()->addIp(*ip))
 						fprintf(stderr,"ERROR: unable to add ip address %s" ZT_EOL_S, ip->toString(ipbuf));
 				}
 			}
 
 #ifdef __APPLE__
-			if (!MacDNSHelper::addIps(n.config.nwid, n.config.mac, n.tap->deviceName().c_str(), newManagedIps))
+			if (!MacDNSHelper::addIps(n.config().nwid, n.config().mac, n.tap()->deviceName().c_str(), newManagedIps))
 				fprintf(stderr, "ERROR: unable to add v6 addresses to system configuration" ZT_EOL_S);
 #endif
 #endif
-			n.managedIps.swap(newManagedIps);
+			n.setManagedIps(newManagedIps);
 		}
 
 		if (syncRoutes) {
 			// Get tap device name (use LUID in hex on Windows) and IP addresses.
 #if defined(__WINDOWS__) && !defined(ZT_SDK)
 			char tapdevbuf[64];
-			OSUtils::ztsnprintf(tapdevbuf,sizeof(tapdevbuf),"%.16llx",(unsigned long long)((WindowsEthernetTap *)(n.tap.get()))->luid().Value);
+			OSUtils::ztsnprintf(tapdevbuf,sizeof(tapdevbuf),"%.16llx",(unsigned long long)((WindowsEthernetTap *)(n.tap().get()))->luid().Value);
 			std::string tapdev(tapdevbuf);
 #else
-			std::string tapdev(n.tap->deviceName());
+			std::string tapdev(n.tap()->deviceName());
 #endif
 
-			std::vector<InetAddress> tapIps(n.tap->ips());
+			std::vector<InetAddress> tapIps(n.tap()->ips());
 			std::set<InetAddress> myIps(tapIps.begin(), tapIps.end());
-			for(unsigned int i=0;i<n.config.assignedAddressCount;++i)
-				myIps.insert(InetAddress(n.config.assignedAddresses[i]));
+			for(unsigned int i=0;i<n.config().assignedAddressCount;++i)
+				myIps.insert(InetAddress(n.config().assignedAddresses[i]));
 
 			std::set<InetAddress> haveRouteTargets;
-			for(unsigned int i=0;i<n.config.routeCount;++i) {
-				const InetAddress *const target = reinterpret_cast<const InetAddress *>(&(n.config.routes[i].target));
-				const InetAddress *const via = reinterpret_cast<const InetAddress *>(&(n.config.routes[i].via));
+			for(unsigned int i=0;i<n.config().routeCount;++i) {
+				const InetAddress *const target = reinterpret_cast<const InetAddress *>(&(n.config().routes[i].target));
+				const InetAddress *const via = reinterpret_cast<const InetAddress *>(&(n.config().routes[i].via));
 
 				// Make sure we are allowed to set this managed route, and that 'via' is not our IP. The latter
 				// avoids setting routes via the router on the router.
@@ -2094,7 +2330,7 @@ public:
 				// Apple on the other hand seems to need this at least on some versions.
 #ifndef __APPLE__
 				bool haveRoute = false;
-				for(std::vector<InetAddress>::iterator ip(n.managedIps.begin());ip!=n.managedIps.end();++ip) {
+				for(std::vector<InetAddress>::iterator ip(n.managedIps().begin());ip!=n.managedIps().end();++ip) {
 					if ((target->netmaskBits() == ip->netmaskBits())&&(target->containsAddress(*ip))) {
 						haveRoute = true;
 						break;
@@ -2107,48 +2343,48 @@ public:
 				haveRouteTargets.insert(*target);
 
 #ifndef ZT_SDK
-				SharedPtr<ManagedRoute> &mr = n.managedRoutes[*target];
+				SharedPtr<ManagedRoute> &mr = n.managedRoutes()[*target];
 				if (!mr)
 					mr.set(new ManagedRoute(*target, *via, *src, tapdev.c_str()));
 #endif
 			}
 
-			for(std::map< InetAddress, SharedPtr<ManagedRoute> >::iterator r(n.managedRoutes.begin());r!=n.managedRoutes.end();) {
+			for(std::map< InetAddress, SharedPtr<ManagedRoute> >::iterator r(n.managedRoutes().begin());r!=n.managedRoutes().end();) {
 				if (haveRouteTargets.find(r->first) == haveRouteTargets.end())
-					n.managedRoutes.erase(r++);
+					n.managedRoutes().erase(r++);
 				else ++r;
 			}
 
 			// Sync device-local managed routes first, then indirect results. That way
 			// we don't get destination unreachable for routes that are via things
 			// that do not yet have routes in the system.
-			for(std::map< InetAddress, SharedPtr<ManagedRoute> >::iterator r(n.managedRoutes.begin());r!=n.managedRoutes.end();++r) {
+			for(std::map< InetAddress, SharedPtr<ManagedRoute> >::iterator r(n.managedRoutes().begin());r!=n.managedRoutes().end();++r) {
 				if (!r->second->via())
 					r->second->sync();
 			}
-			for(std::map< InetAddress, SharedPtr<ManagedRoute> >::iterator r(n.managedRoutes.begin());r!=n.managedRoutes.end();++r) {
+			for(std::map< InetAddress, SharedPtr<ManagedRoute> >::iterator r(n.managedRoutes().begin());r!=n.managedRoutes().end();++r) {
 				if (r->second->via())
 					r->second->sync();
 			}
 		}
 
 		if (syncDns) {
-			if (n.settings.allowDNS) {
-				if (strlen(n.config.dns.domain) != 0) {
+			if (n.allowDNS()) {
+				if (strlen(n.config().dns.domain) != 0) {
 					std::vector<InetAddress> servers;
 					for (int j = 0; j < ZT_MAX_DNS_SERVERS; ++j) {
-						InetAddress a(n.config.dns.server_addr[j]);
+						InetAddress a(n.config().dns.server_addr[j]);
 						if (a.isV4() || a.isV6()) {
 							servers.push_back(a);
 						}
 					}
-					n.tap->setDns(n.config.dns.domain, servers);
+					n.tap()->setDns(n.config().dns.domain, servers);
 				}
 			} else {
 #ifdef __APPLE__
-				MacDNSHelper::removeDNS(n.config.nwid);
+				MacDNSHelper::removeDNS(n.config().nwid);
 #elif defined(__WINDOWS__)
-				WinDNSHelper::removeDNS(n.config.nwid);
+				WinDNSHelper::removeDNS(n.config().nwid);
 #endif
 			}
 
@@ -2417,16 +2653,16 @@ public:
 	{
 		Mutex::Lock _l(_nets_m);
 		NetworkState &n = _nets[nwid];
+		n.setWebPort(_primaryPort);
 
-		switch(op) {
-
+		switch (op) {
 			case ZT_VIRTUAL_NETWORK_CONFIG_OPERATION_UP:
-				if (!n.tap) {
+				if (!n.tap()) {
 					try {
 						char friendlyName[128];
 						OSUtils::ztsnprintf(friendlyName,sizeof(friendlyName),"ZeroTier One [%.16llx]",nwid);
 
-						n.tap = EthernetTap::newInstance(
+						n.setTap(EthernetTap::newInstance(
 							nullptr,
 							_homePath.c_str(),
 							MAC(nwc->mac),
@@ -2435,7 +2671,7 @@ public:
 							nwid,
 							friendlyName,
 							StapFrameHandler,
-							(void *)this);
+							(void *)this));
 						*nuptr = (void *)&n;
 
 						char nlcpath[256];
@@ -2449,28 +2685,28 @@ public:
 								std::string addresses (allowManaged.begin(), allowManaged.size());
 								if (allowManaged.size() <= 5) { // untidy parsing for backward compatibility
 									if (allowManaged[0] == '1' || allowManaged[0] == 't' || allowManaged[0] == 'T') {
-										n.settings.allowManaged = true;
+										n.setAllowManaged(true);
 									} else {
-										n.settings.allowManaged = false;
+										n.setAllowManaged(false);
 									}
 								} else {
 									// this should be a list of IP addresses
-									n.settings.allowManaged = true;
+									n.setAllowManaged(true);
 									size_t pos = 0;
 									while (true) {
 										size_t nextPos = addresses.find(',', pos);
 										std::string address = addresses.substr(pos, (nextPos == std::string::npos ? addresses.size() : nextPos) - pos);
-										n.settings.allowManagedWhitelist.push_back(InetAddress(address.c_str()));
+										n.addToAllowManagedWhiteList(InetAddress(address.c_str()));
 										if (nextPos == std::string::npos) break;
 										pos = nextPos + 1;
 									}
 								}
 							} else {
-								n.settings.allowManaged = true;
+								n.setAllowManaged(true);
 							}
-							n.settings.allowGlobal = nc.getB("allowGlobal", false);
-							n.settings.allowDefault = nc.getB("allowDefault", false);
-							n.settings.allowDNS = nc.getB("allowDNS", false);
+							n.setAllowGlobal(nc.getB("allowGlobal", false));
+							n.setAllowDefault(nc.getB("allowDefault", false));
+							n.setAllowDNS(nc.getB("allowDNS", false));
 						}
 					} catch (std::exception &exc) {
 #ifdef __WINDOWS__
@@ -2491,20 +2727,21 @@ public:
 				// After setting up tap, fall through to CONFIG_UPDATE since we also want to do this...
 
 			case ZT_VIRTUAL_NETWORK_CONFIG_OPERATION_CONFIG_UPDATE:
-				memcpy(&(n.config),nwc,sizeof(ZT_VirtualNetworkConfig));
-				if (n.tap) { // sanity check
+				n.setConfig(nwc);
+
+				if (n.tap()) { // sanity check
 #if defined(__WINDOWS__) && !defined(ZT_SDK)
 					// wait for up to 5 seconds for the WindowsEthernetTap to actually be initialized
 					//
 					// without WindowsEthernetTap::isInitialized() returning true, the won't actually
 					// be online yet and setting managed routes on it will fail.
 					const int MAX_SLEEP_COUNT = 500;
-					for (int i = 0; !((WindowsEthernetTap *)(n.tap.get()))->isInitialized() && i < MAX_SLEEP_COUNT; i++) {
+					for (int i = 0; !((WindowsEthernetTap *)(n.tap().get()))->isInitialized() && i < MAX_SLEEP_COUNT; i++) {
 						Sleep(10);
 					}
 #endif
 					syncManagedStuff(n,true,true,true);
-					n.tap->setMtu(nwc->mtu);
+					n.tap()->setMtu(nwc->mtu);
 				} else {
 					_nets.erase(nwid);
 					return -999; // tap init failed
@@ -2513,12 +2750,12 @@ public:
 
 			case ZT_VIRTUAL_NETWORK_CONFIG_OPERATION_DOWN:
 			case ZT_VIRTUAL_NETWORK_CONFIG_OPERATION_DESTROY:
-				if (n.tap) { // sanity check
+				if (n.tap()) { // sanity check
 #if defined(__WINDOWS__) && !defined(ZT_SDK)
-					std::string winInstanceId(((WindowsEthernetTap *)(n.tap.get()))->instanceId());
+					std::string winInstanceId(((WindowsEthernetTap *)(n.tap().get()))->instanceId());
 #endif
 					*nuptr = (void *)0;
-					n.tap.reset();
+					n.tap().reset();
 					_nets.erase(nwid);
 #if defined(__WINDOWS__) && !defined(ZT_SDK)
 					if ((op == ZT_VIRTUAL_NETWORK_CONFIG_OPERATION_DESTROY)&&(winInstanceId.length() > 0))
@@ -2533,7 +2770,6 @@ public:
 					_nets.erase(nwid);
 				}
 				break;
-
 		}
 		return 0;
 	}
@@ -2933,9 +3169,9 @@ public:
 	inline void nodeVirtualNetworkFrameFunction(uint64_t nwid,void **nuptr,uint64_t sourceMac,uint64_t destMac,unsigned int etherType,unsigned int vlanId,const void *data,unsigned int len)
 	{
 		NetworkState *n = reinterpret_cast<NetworkState *>(*nuptr);
-		if ((!n)||(!n->tap))
+		if ((!n)||(!n->tap()))
 			return;
-		n->tap->put(MAC(sourceMac),MAC(destMac),etherType,data,len);
+		n->tap()->put(MAC(sourceMac),MAC(destMac),etherType,data,len);
 	}
 
 	inline int nodePathCheckFunction(uint64_t ztaddr,const int64_t localSocket,const struct sockaddr_storage *remoteAddr)
@@ -2944,8 +3180,8 @@ public:
 		{
 			Mutex::Lock _l(_nets_m);
 			for(std::map<uint64_t,NetworkState>::const_iterator n(_nets.begin());n!=_nets.end();++n) {
-				if (n->second.tap) {
-					std::vector<InetAddress> ips(n->second.tap->ips());
+				if (n->second.tap()) {
+					std::vector<InetAddress> ips(n->second.tap()->ips());
 					for(std::vector<InetAddress>::const_iterator i(ips.begin());i!=ips.end();++i) {
 						if (i->containsAddress(*(reinterpret_cast<const InetAddress *>(remoteAddr)))) {
 							return 0;
@@ -3117,14 +3353,14 @@ public:
 		{
 			Mutex::Lock _l(_nets_m);
 			for(std::map<uint64_t,NetworkState>::const_iterator n(_nets.begin());n!=_nets.end();++n) {
-				if (n->second.tap) {
-					std::vector<InetAddress> ips(n->second.tap->ips());
+				if (n->second.tap()) {
+					std::vector<InetAddress> ips(n->second.tap()->ips());
 					for(std::vector<InetAddress>::const_iterator i(ips.begin());i!=ips.end();++i) {
 						if (i->ipsEqual(ifaddr))
 							return false;
 					}
 #ifdef _WIN32
-					if (n->second.tap->friendlyName() == ifname)
+					if (n->second.tap()->friendlyName() == ifname)
 						return false;
 #endif
 				}

+ 5 - 0
windows/ZeroTierOne.sln

@@ -4,9 +4,14 @@ Microsoft Visual Studio Solution File, Format Version 12.00
 VisualStudioVersion = 16.0.30517.126
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ZeroTierOne", "ZeroTierOne\ZeroTierOne.vcxproj", "{B00A4957-5977-4AC1-9EF4-571DC27EADA2}"
+	ProjectSection(ProjectDependencies) = postProject
+		{175C340F-F5BA-4CB1-88AD-533B102E3799} = {175C340F-F5BA-4CB1-88AD-533B102E3799}
+	EndProjectSection
 EndProject
 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TapDriver6", "TapDriver6\TapDriver6.vcxproj", "{43BA7584-D4DB-4F7C-90FC-E2B18A68A213}"
 EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "zeroidc", "..\zeroidc\zeroidc.vcxproj", "{175C340F-F5BA-4CB1-88AD-533B102E3799}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU

+ 17 - 19
windows/ZeroTierOne/ZeroTierOne.vcxproj

@@ -328,8 +328,7 @@
       <WarningLevel>Level3</WarningLevel>
       <Optimization>Disabled</Optimization>
       <SDLCheck>true</SDLCheck>
-      <AdditionalIncludeDirectories>
-      </AdditionalIncludeDirectories>
+      <AdditionalIncludeDirectories>$(SolutionDir)\..\zeroidc\target;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions>ZT_EXPORT;FD_SETSIZE=1024;NOMINMAX;STATICLIB;WIN32;ZT_TRACE;ZT_USE_MINIUPNPC;MINIUPNP_STATICLIB;ZT_SOFTWARE_UPDATE_DEFAULT="disable";%(PreprocessorDefinitions)</PreprocessorDefinitions>
       <DisableSpecificWarnings>4996</DisableSpecificWarnings>
       <RuntimeTypeInfo>false</RuntimeTypeInfo>
@@ -338,8 +337,9 @@
     </ClCompile>
     <Link>
       <GenerateDebugInformation>true</GenerateDebugInformation>
-      <AdditionalDependencies>wsock32.lib;ws2_32.lib;Iphlpapi.lib;Rpcrt4.lib;%(AdditionalDependencies)</AdditionalDependencies>
+      <AdditionalDependencies>wsock32.lib;ws2_32.lib;Iphlpapi.lib;Rpcrt4.lib;zeroidc.lib;bcrypt.lib;userenv.lib;crypt32.lib;secur32.lib;ncrypt.lib;ntdll.lib;%(AdditionalDependencies)</AdditionalDependencies>
       <ImageHasSafeExceptionHandlers>false</ImageHasSafeExceptionHandlers>
+      <AdditionalLibraryDirectories>$(SolutionDir)\..\zeroidc\target\i686-pc-windows-msvc\debug\;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
     </Link>
   </ItemDefinitionGroup>
   <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Profile|Win32'">
@@ -347,8 +347,7 @@
       <WarningLevel>Level3</WarningLevel>
       <Optimization>Disabled</Optimization>
       <SDLCheck>true</SDLCheck>
-      <AdditionalIncludeDirectories>
-      </AdditionalIncludeDirectories>
+      <AdditionalIncludeDirectories>$(SolutionDir)\..\zeroidc\target;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions>ZT_EXPORT;FD_SETSIZE=1024;NOMINMAX;STATICLIB;WIN32;ZT_TRACE;ZT_USE_MINIUPNPC;MINIUPNP_STATICLIB;ZT_SOFTWARE_UPDATE_DEFAULT="disable";%(PreprocessorDefinitions)</PreprocessorDefinitions>
       <DisableSpecificWarnings>4996</DisableSpecificWarnings>
       <RuntimeTypeInfo>false</RuntimeTypeInfo>
@@ -365,8 +364,7 @@
       <WarningLevel>Level3</WarningLevel>
       <Optimization>Disabled</Optimization>
       <SDLCheck>true</SDLCheck>
-      <AdditionalIncludeDirectories>
-      </AdditionalIncludeDirectories>
+      <AdditionalIncludeDirectories>$(SolutionDir)\..\zeroidc\target;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions>ZT_EXPORT;FD_SETSIZE=1024;NOMINMAX;STATICLIB;WIN32;ZT_TRACE;ZT_RULES_ENGINE_DEBUGGING;ZT_USE_MINIUPNPC;MINIUPNP_STATICLIB;ZT_SOFTWARE_UPDATE_DEFAULT="disable";%(PreprocessorDefinitions)</PreprocessorDefinitions>
       <MultiProcessorCompilation>true</MultiProcessorCompilation>
       <DisableSpecificWarnings>4996</DisableSpecificWarnings>
@@ -375,9 +373,10 @@
     </ClCompile>
     <Link>
       <GenerateDebugInformation>true</GenerateDebugInformation>
-      <AdditionalDependencies>wbemuuid.lib;wsock32.lib;ws2_32.lib;Iphlpapi.lib;Rpcrt4.lib;%(AdditionalDependencies)</AdditionalDependencies>
+      <AdditionalDependencies>wbemuuid.lib;wsock32.lib;ws2_32.lib;Iphlpapi.lib;Rpcrt4.lib;zeroidc.lib;bcrypt.lib;userenv.lib;crypt32.lib;secur32.lib;ncrypt.lib;ntdll.lib;%(AdditionalDependencies)</AdditionalDependencies>
       <ImageHasSafeExceptionHandlers>false</ImageHasSafeExceptionHandlers>
       <AdditionalOptions>"notelemetry.obj" %(AdditionalOptions)</AdditionalOptions>
+      <AdditionalLibraryDirectories>$(SolutionDir)..\zeroidc\target\x86_64-pc-windows-msvc\debug\;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
     </Link>
   </ItemDefinitionGroup>
   <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Profile|x64'">
@@ -385,8 +384,7 @@
       <WarningLevel>Level3</WarningLevel>
       <Optimization>Disabled</Optimization>
       <SDLCheck>true</SDLCheck>
-      <AdditionalIncludeDirectories>
-      </AdditionalIncludeDirectories>
+      <AdditionalIncludeDirectories>$(SolutionDir)\..\zeroidc\target;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions>ZT_EXPORT;FD_SETSIZE=1024;NOMINMAX;STATICLIB;WIN32;ZT_USE_MINIUPNPC;MINIUPNP_STATICLIB;ZT_SOFTWARE_UPDATE_DEFAULT="disable";%(PreprocessorDefinitions)</PreprocessorDefinitions>
       <MultiProcessorCompilation>false</MultiProcessorCompilation>
       <DisableSpecificWarnings>4996</DisableSpecificWarnings>
@@ -407,10 +405,9 @@
       <FunctionLevelLinking>true</FunctionLevelLinking>
       <IntrinsicFunctions>true</IntrinsicFunctions>
       <SDLCheck>true</SDLCheck>
-      <AdditionalIncludeDirectories>
-      </AdditionalIncludeDirectories>
+      <AdditionalIncludeDirectories>$(SolutionDir)\..\zeroidc\target;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions>ZT_EXPORT;FD_SETSIZE=1024;STATICLIB;ZT_SALSA20_SSE;ZT_USE_MINIUPNPC;MINIUPNP_STATICLIB;WIN32;NOMINMAX;ZT_SOFTWARE_UPDATE_DEFAULT="apply";ZT_BUILD_PLATFORM=2;ZT_BUILD_ARCHITECTURE=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
-      <RuntimeLibrary>MultiThreaded</RuntimeLibrary>
+      <RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
       <EnableEnhancedInstructionSet>StreamingSIMDExtensions2</EnableEnhancedInstructionSet>
       <StringPooling>true</StringPooling>
       <InlineFunctionExpansion>AnySuitable</InlineFunctionExpansion>
@@ -429,8 +426,9 @@
       <GenerateDebugInformation>false</GenerateDebugInformation>
       <EnableCOMDATFolding>true</EnableCOMDATFolding>
       <OptimizeReferences>true</OptimizeReferences>
-      <AdditionalDependencies>wsock32.lib;ws2_32.lib;Iphlpapi.lib;Rpcrt4.lib;%(AdditionalDependencies)</AdditionalDependencies>
+      <AdditionalDependencies>wsock32.lib;ws2_32.lib;Iphlpapi.lib;Rpcrt4.lib;zeroidc.lib;bcrypt.lib;userenv.lib;crypt32.lib;secur32.lib;ncrypt.lib;ntdll.lib;%(AdditionalDependencies)</AdditionalDependencies>
       <ImageHasSafeExceptionHandlers>false</ImageHasSafeExceptionHandlers>
+      <AdditionalLibraryDirectories>$(SolutionDir)..\zeroidc\target\i686-pc-windows-msvc\release\;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
     </Link>
   </ItemDefinitionGroup>
   <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
@@ -440,10 +438,9 @@
       <FunctionLevelLinking>true</FunctionLevelLinking>
       <IntrinsicFunctions>true</IntrinsicFunctions>
       <SDLCheck>true</SDLCheck>
-      <AdditionalIncludeDirectories>
-      </AdditionalIncludeDirectories>
+      <AdditionalIncludeDirectories>$(SolutionDir)\..\zeroidc\target;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions>ZT_EXPORT;FD_SETSIZE=1024;STATICLIB;ZT_SOFTWARE_UPDATE_DEFAULT="apply";ZT_SALSA20_SSE;ZT_USE_MINIUPNPC;MINIUPNP_STATICLIB;WIN32;NOMINMAX;ZT_BUILD_PLATFORM=2;ZT_BUILD_ARCHITECTURE=2;%(PreprocessorDefinitions)</PreprocessorDefinitions>
-      <RuntimeLibrary>MultiThreaded</RuntimeLibrary>
+      <RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
       <EnableEnhancedInstructionSet>StreamingSIMDExtensions2</EnableEnhancedInstructionSet>
       <StringPooling>true</StringPooling>
       <InlineFunctionExpansion>AnySuitable</InlineFunctionExpansion>
@@ -452,7 +449,7 @@
       <DisableSpecificWarnings>4996</DisableSpecificWarnings>
       <ControlFlowGuard>Guard</ControlFlowGuard>
       <EnableParallelCodeGeneration>false</EnableParallelCodeGeneration>
-      <CallingConvention>VectorCall</CallingConvention>
+      <CallingConvention>Cdecl</CallingConvention>
       <RuntimeTypeInfo>false</RuntimeTypeInfo>
       <LanguageStandard>stdcpp14</LanguageStandard>
       <DebugInformationFormat>None</DebugInformationFormat>
@@ -464,8 +461,9 @@
       <GenerateDebugInformation>false</GenerateDebugInformation>
       <EnableCOMDATFolding>true</EnableCOMDATFolding>
       <OptimizeReferences>true</OptimizeReferences>
-      <AdditionalDependencies>wbemuuid.lib;wsock32.lib;ws2_32.lib;Iphlpapi.lib;Rpcrt4.lib;%(AdditionalDependencies)</AdditionalDependencies>
+      <AdditionalDependencies>wbemuuid.lib;wsock32.lib;ws2_32.lib;Iphlpapi.lib;Rpcrt4.lib;zeroidc.lib;bcrypt.lib;userenv.lib;crypt32.lib;secur32.lib;ncrypt.lib;ntdll.lib;%(AdditionalDependencies)</AdditionalDependencies>
       <ImageHasSafeExceptionHandlers>false</ImageHasSafeExceptionHandlers>
+      <AdditionalLibraryDirectories>$(SolutionDir)..\zeroidc\target\x86_64-pc-windows-msvc\release\;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
     </Link>
   </ItemDefinitionGroup>
   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />

+ 5 - 0
zeroidc/.cargo/config.toml

@@ -0,0 +1,5 @@
+[target.x86_64-apple-darwin]
+rustflags=["-C", "link-arg=-mmacosx-version-min=10.13"]
+
+[target.aarch64-apple-darwin]
+rustflags=["-C", "link-arg=-mmacosx-version-min=10.13"]

+ 1506 - 0
zeroidc/Cargo.lock

@@ -0,0 +1,1506 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "ansi_term"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+
+[[package]]
+name = "base64"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
+
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "block-buffer"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c"
+
+[[package]]
+name = "bytes"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
+
+[[package]]
+name = "cbindgen"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51e3973b165dc0f435831a9e426de67e894de532754ff7a3f307c03ee5dec7dc"
+dependencies = [
+ "clap",
+ "heck",
+ "indexmap",
+ "log",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "syn",
+ "tempfile",
+ "toml",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
+dependencies = [
+ "libc",
+ "num-integer",
+ "num-traits",
+ "serde",
+ "time 0.1.43",
+ "winapi",
+]
+
+[[package]]
+name = "clap"
+version = "2.33.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
+dependencies = [
+ "ansi_term",
+ "atty",
+ "bitflags",
+ "strsim",
+ "textwrap",
+ "unicode-width",
+ "vec_map",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "digest"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "either"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
+dependencies = [
+ "matches",
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
+
+[[package]]
+name = "futures-io"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11"
+
+[[package]]
+name = "futures-task"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99"
+
+[[package]]
+name = "futures-util"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
+dependencies = [
+ "autocfg",
+ "futures-core",
+ "futures-io",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "h2"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fd819562fcebdac5afc5c113c3ec36f902840b70fd4fc458799c8ce4607ae55"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+
+[[package]]
+name = "heck"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "http"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6"
+dependencies = [
+ "bytes",
+ "http",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503"
+
+[[package]]
+name = "httpdate"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440"
+
+[[package]]
+name = "hyper"
+version = "0.14.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b91bb1f221b6ea1f1e4371216b70f40748774c2fb5971b450c07773fb92d26b"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac"
+dependencies = [
+ "http",
+ "hyper",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes",
+ "hyper",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
+[[package]]
+name = "idna"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
+dependencies = [
+ "matches",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
+
+[[package]]
+name = "itertools"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+
+[[package]]
+name = "js-sys"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "jsonwebtoken"
+version = "7.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afabcc15e437a6484fc4f12d0fd63068fe457bf93f1c148d3d9649c60b103f32"
+dependencies = [
+ "base64 0.12.3",
+ "pem",
+ "ring",
+ "serde",
+ "serde_json",
+ "simple_asn1",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.105"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "869d572136620d55835903746bcb5cdc54cb2851fd0aeec53220b4bb65ef3013"
+
+[[package]]
+name = "log"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
+
+[[package]]
+name = "memchr"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
+
+[[package]]
+name = "mime"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+
+[[package]]
+name = "mio"
+version = "0.7.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
+dependencies = [
+ "libc",
+ "log",
+ "miow",
+ "ntapi",
+ "winapi",
+]
+
+[[package]]
+name = "miow"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "ntapi"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "oauth2"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80e47cfc4c0a1a519d9a025ebfbac3a2439d1b5cdf397d72dcb79b11d9920dab"
+dependencies = [
+ "base64 0.13.0",
+ "chrono",
+ "getrandom",
+ "http",
+ "rand",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "serde_path_to_error",
+ "sha2",
+ "thiserror",
+ "url",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+
+[[package]]
+name = "openidconnect"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d523cf32bdf7696f36bc4198a42c34b65f0227b97f2f501ebfbe016baa5bc52"
+dependencies = [
+ "base64 0.13.0",
+ "chrono",
+ "http",
+ "itertools",
+ "log",
+ "num-bigint 0.4.3",
+ "oauth2",
+ "rand",
+ "ring",
+ "serde",
+ "serde-value",
+ "serde_derive",
+ "serde_json",
+ "serde_path_to_error",
+ "thiserror",
+ "untrusted",
+ "url",
+]
+
+[[package]]
+name = "openssl"
+version = "0.10.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7df13d165e607909b363a4757a6f133f8a818a74e9d3a98d09c6128e15fa4c73"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "ordered-float"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3305af35278dd29f46fcdd139e0b1fbfae2153f0e5928b39b035542dd31e37b7"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "pem"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb"
+dependencies = [
+ "base64 0.13.0",
+ "once_cell",
+ "regex",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+ "rand_hc",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
+dependencies = [
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.11.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bea77bc708afa10e59905c3d4af7c8fd43c9214251673095ff8b14345fcbc5"
+dependencies = [
+ "base64 0.13.0",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-rustls",
+ "hyper-tls",
+ "ipnet",
+ "js-sys",
+ "lazy_static",
+ "log",
+ "mime",
+ "native-tls",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls",
+ "rustls-pemfile",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "tokio",
+ "tokio-native-tls",
+ "tokio-rustls",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "webpki-roots",
+ "winreg",
+]
+
+[[package]]
+name = "ring"
+version = "0.16.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
+dependencies = [
+ "cc",
+ "libc",
+ "once_cell",
+ "spin",
+ "untrusted",
+ "web-sys",
+ "winapi",
+]
+
+[[package]]
+name = "rustls"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d37e5e2290f3e040b594b1a9e04377c2c671f1a1cfd9bfdef82106ac1c113f84"
+dependencies = [
+ "log",
+ "ring",
+ "sct",
+ "webpki 0.22.0",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9"
+dependencies = [
+ "base64 0.13.0",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+
+[[package]]
+name = "schannel"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
+dependencies = [
+ "lazy_static",
+ "winapi",
+]
+
+[[package]]
+name = "sct"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "security-framework"
+version = "2.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.130"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-value"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a65a7291a8a568adcae4c10a677ebcedbc6c9cec91c054dee2ce40b0e3290eb"
+dependencies = [
+ "ordered-float",
+ "serde",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.130"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0421d4f173fab82d72d6babf36d57fae38b994ca5c2d78e704260ba6d12118b"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha2"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa"
+dependencies = [
+ "block-buffer",
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+ "opaque-debug",
+]
+
+[[package]]
+name = "simple_asn1"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "692ca13de57ce0613a363c8c2f1de925adebc81b04c923ac60c5488bb44abe4b"
+dependencies = [
+ "chrono",
+ "num-bigint 0.2.6",
+ "num-traits",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
+
+[[package]]
+name = "socket2"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "spin"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+
+[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+
+[[package]]
+name = "syn"
+version = "1.0.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "rand",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "time"
+version = "0.1.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "time"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41effe7cfa8af36f439fac33861b66b049edc6f9a32331e2312660529c1c24ad"
+dependencies = [
+ "itoa",
+ "libc",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
+[[package]]
+name = "tokio"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc"
+dependencies = [
+ "autocfg",
+ "bytes",
+ "libc",
+ "memchr",
+ "mio",
+ "num_cpus",
+ "pin-project-lite",
+ "winapi",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4baa378e417d780beff82bf54ceb0d195193ea6a00c14e22359e7f39456b5689"
+dependencies = [
+ "rustls",
+ "tokio",
+ "webpki 0.22.0",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d3725d3efa29485e87311c5b699de63cde14b00ed4d256b8318aa30ca452cd"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "log",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "tower-service"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
+
+[[package]]
+name = "tracing"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105"
+dependencies = [
+ "cfg-if",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
+
+[[package]]
+name = "typenum"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+
+[[package]]
+name = "untrusted"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+
+[[package]]
+name = "url"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "matches",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
+[[package]]
+name = "version_check"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
+
+[[package]]
+name = "want"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
+dependencies = [
+ "log",
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b"
+dependencies = [
+ "bumpalo",
+ "lazy_static",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"
+
+[[package]]
+name = "web-sys"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki"
+version = "0.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "webpki"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940"
+dependencies = [
+ "webpki 0.21.4",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "winreg"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "zeroidc"
+version = "0.1.0"
+dependencies = [
+ "base64 0.13.0",
+ "bytes",
+ "cbindgen",
+ "jsonwebtoken",
+ "openidconnect",
+ "reqwest",
+ "serde",
+ "thiserror",
+ "time 0.3.5",
+ "url",
+]

+ 26 - 0
zeroidc/Cargo.toml

@@ -0,0 +1,26 @@
+[package]
+name = "zeroidc"
+version = "0.1.0"
+edition = "2018"
+build = "build.rs"
+publish = false
+
+[lib]
+crate-type = ["staticlib","rlib"]
+
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+openidconnect = "2.1"
+base64 = "0.13"
+url = "2.2"
+reqwest = "0.11"
+jsonwebtoken = "7.2"
+serde = "1.0"
+time = { version = "0.3", features = ["formatting"] }
+bytes = "1.1"
+thiserror = "1"
+
+[build-dependencies]
+cbindgen = "0.20"

+ 37 - 0
zeroidc/build.rs

@@ -0,0 +1,37 @@
+extern crate cbindgen;
+
+use std::env;
+use std::path::PathBuf;
+use cbindgen::{Config, Language};
+
+fn main() {
+    let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
+
+    let package_name = env::var("CARGO_PKG_NAME").unwrap();
+    let output_file = target_dir()
+        .join(format!("{}.h", package_name))
+        .display()
+        .to_string();
+
+    let config = Config {
+        language: Language::C,
+        cpp_compat: true,
+        namespace: Some(String::from("zeroidc")),
+        ..Default::default()
+    };
+
+    cbindgen::generate_with_config(&crate_dir, config)
+      .unwrap()
+      .write_to_file(&output_file);
+}
+
+/// Find the location of the `target/` directory. Note that this may be 
+/// overridden by `cmake`, so we also need to check the `CARGO_TARGET_DIR` 
+/// variable.
+fn target_dir() -> PathBuf {
+    if let Ok(target) = env::var("CARGO_TARGET_DIR") {
+        PathBuf::from(target)
+    } else {
+        PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("target")
+    }
+}

+ 23 - 0
zeroidc/src/error.rs

@@ -0,0 +1,23 @@
+/*
+ * Copyright (c)2022 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: 2025-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.
+ */
+
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum ZeroIDCError
+{
+    #[error(transparent)]
+    DiscoveryError(#[from] openidconnect::DiscoveryError<openidconnect::reqwest::Error<reqwest::Error>>),
+
+    #[error(transparent)]
+    ParseError(#[from] url::ParseError),
+}

+ 220 - 0
zeroidc/src/ext.rs

@@ -0,0 +1,220 @@
+/*
+ * Copyright (c)2021 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: 2025-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.
+ */
+
+use std::ffi::{CStr, CString};
+use std::os::raw::c_char;
+use url::{Url};
+
+use crate::ZeroIDC;
+
+#[no_mangle]
+pub extern "C" fn zeroidc_new(
+    issuer: *const c_char,
+    client_id: *const c_char,
+    auth_endpoint: *const c_char,
+    web_listen_port: u16,
+) -> *mut ZeroIDC {
+    if issuer.is_null() {
+        println!("issuer is null");
+        return std::ptr::null_mut();
+    }
+
+    if client_id.is_null() {
+        println!("client_id is null");
+        return std::ptr::null_mut();
+    }
+
+    if auth_endpoint.is_null() {
+        println!("auth_endpoint is null");
+        return std::ptr::null_mut();
+    }
+
+    let issuer = unsafe { CStr::from_ptr(issuer) };
+    let client_id = unsafe { CStr::from_ptr(client_id) };
+    let auth_endpoint = unsafe { CStr::from_ptr(auth_endpoint) };
+    match ZeroIDC::new(
+        issuer.to_str().unwrap(),
+        client_id.to_str().unwrap(),
+        auth_endpoint.to_str().unwrap(),
+        web_listen_port,
+    ) {
+        Ok(idc) => {
+            return Box::into_raw(Box::new(idc));
+        }
+        Err(s) => {
+            println!("Error creating ZeroIDC instance: {}", s);
+            return std::ptr::null_mut();
+        }
+    }
+}
+
+#[no_mangle]
+pub extern "C" fn zeroidc_delete(ptr: *mut ZeroIDC) {
+    if ptr.is_null() {
+        return;
+    }
+    unsafe {
+        Box::from_raw(ptr);
+    }
+}
+
+#[no_mangle]
+pub extern "C" fn zeroidc_start(ptr: *mut ZeroIDC) {
+    let idc = unsafe {
+        assert!(!ptr.is_null());
+        &mut *ptr
+    };
+    idc.start();
+}
+
+#[no_mangle]
+pub extern "C" fn zeroidc_stop(ptr: *mut ZeroIDC) {
+    let idc = unsafe {
+        assert!(!ptr.is_null());
+        &mut *ptr
+    };
+    idc.stop();
+}
+
+#[no_mangle]
+pub extern "C" fn zeroidc_is_running(ptr: *mut ZeroIDC) -> bool {
+    let idc = unsafe {
+        assert!(!ptr.is_null());
+        &mut *ptr
+    };
+
+    idc.is_running()
+}
+
+#[no_mangle]
+pub extern "C" fn zeroidc_get_exp_time(ptr: *mut ZeroIDC) -> u64 {
+    let id = unsafe {
+        assert!(!ptr.is_null());
+        &mut *ptr
+    };
+
+    id.get_exp_time()
+}
+
+#[no_mangle]
+pub extern "C" fn zeroidc_set_nonce_and_csrf(
+    ptr: *mut ZeroIDC,
+    csrf_token: *const c_char,
+    nonce: *const c_char) {
+    let idc = unsafe {
+        assert!(!ptr.is_null());
+        &mut *ptr
+    };
+
+    if csrf_token.is_null() {
+        println!("csrf_token is null");
+        return;
+    }
+
+    if nonce.is_null() {
+        println!("nonce is null");
+        return;
+    }
+
+    let csrf_token = unsafe { CStr::from_ptr(csrf_token) }
+        .to_str()
+        .unwrap()
+        .to_string();
+    let nonce = unsafe { CStr::from_ptr(nonce) }
+        .to_str()
+        .unwrap()
+        .to_string();
+     
+    idc.set_nonce_and_csrf(csrf_token, nonce);
+}
+
+#[no_mangle]
+pub extern "C" fn zeroidc_get_auth_url(ptr: *mut ZeroIDC) -> *const c_char {
+    if ptr.is_null() {
+        println!("passed a null object");
+        return std::ptr::null_mut();
+    }
+    let idc = unsafe {
+        &mut *ptr
+    };
+    
+    let s = CString::new(idc.auth_url()).unwrap();
+    return s.into_raw();
+}
+
+#[no_mangle]
+pub extern "C" fn zeroidc_token_exchange(idc: *mut ZeroIDC, code: *const c_char ) -> *const c_char {
+    if idc.is_null() {
+        println!("idc is null");
+        return std::ptr::null();
+    }
+
+    if code.is_null() {
+        println!("code is null");
+        return std::ptr::null();
+    }
+    let idc = unsafe {
+        &mut *idc
+    };
+
+    let code = unsafe{CStr::from_ptr(code)}.to_str().unwrap();
+
+    let ret = idc.do_token_exchange( code);
+    let ret = CString::new(ret).unwrap();
+    return ret.into_raw();
+}
+
+#[no_mangle]
+pub extern "C" fn zeroidc_get_url_param_value(param: *const c_char, path: *const c_char) -> *const c_char {
+    if param.is_null() {
+        println!("param is null");
+        return std::ptr::null();
+    }
+    if path.is_null() {
+        println!("path is null");
+        return std::ptr::null();
+    }
+    let param = unsafe {CStr::from_ptr(param)}.to_str().unwrap();
+    let path =  unsafe {CStr::from_ptr(path)}.to_str().unwrap();
+
+    let url = "http://localhost:9993".to_string() + path;
+    let url = Url::parse(&url).unwrap();
+
+    let pairs = url.query_pairs();  
+    for p in pairs {
+        if p.0 == param {
+            let s = CString::new(p.1.into_owned()).unwrap();
+            return s.into_raw()
+        }
+    }
+
+    return std::ptr::null();
+}
+
+#[no_mangle]
+pub extern "C" fn zeroidc_network_id_from_state(state: *const c_char) -> *const c_char {
+    if state.is_null() {
+        println!("state is null");
+        return std::ptr::null();
+    }
+
+    let state = unsafe{CStr::from_ptr(state)}.to_str().unwrap();
+
+    let split = state.split("_");
+    let split = split.collect::<Vec<&str>>();
+    if split.len() != 2 {
+        return std::ptr::null();
+    }
+
+    let s = CString::new(split[1]).unwrap();
+    return s.into_raw();
+}

+ 582 - 0
zeroidc/src/lib.rs

@@ -0,0 +1,582 @@
+/*
+ * Copyright (c)2021 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: 2025-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.
+ */
+
+pub mod error;
+pub mod ext;
+
+extern crate base64;
+extern crate bytes;
+extern crate openidconnect;
+extern crate time;
+extern crate url;
+
+use crate::error::ZeroIDCError;
+
+use bytes::Bytes;
+use jsonwebtoken::{dangerous_insecure_decode};
+use openidconnect::core::{CoreClient, CoreProviderMetadata, CoreResponseType};
+use openidconnect::reqwest::http_client;
+use openidconnect::{AccessToken, AccessTokenHash, AuthorizationCode, AuthenticationFlow, ClientId, CsrfToken, IssuerUrl, Nonce, OAuth2TokenResponse, PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, RefreshToken, Scope, TokenResponse};
+use serde::{Deserialize, Serialize};
+use std::str::from_utf8;
+use std::sync::{Arc, Mutex};
+use std::thread::{sleep, spawn, JoinHandle};
+use std::time::{SystemTime, UNIX_EPOCH, Duration};
+use time::{OffsetDateTime, format_description};
+
+
+use url::Url;
+
+pub struct ZeroIDC {
+    inner: Arc<Mutex<Inner>>,
+}
+
+struct Inner {
+    running: bool,
+    auth_endpoint: String,
+    oidc_thread: Option<JoinHandle<()>>,
+    oidc_client: Option<openidconnect::core::CoreClient>,
+    access_token: Option<AccessToken>,
+    refresh_token: Option<RefreshToken>,
+    exp_time: u64,
+
+    url: Option<Url>,
+    csrf_token: Option<CsrfToken>,
+    nonce: Option<Nonce>,
+    pkce_verifier: Option<PkceCodeVerifier>,
+}
+
+impl Inner {
+    #[inline]
+    fn as_opt(&mut self) -> Option<&mut Inner> {
+        Some(self)
+    }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+struct Exp {
+    exp: u64
+}
+
+fn csrf_func(csrf_token: String) -> Box<dyn Fn() -> CsrfToken> {
+    return Box::new(move || CsrfToken::new(csrf_token.to_string()));
+}
+
+fn nonce_func(nonce: String) -> Box<dyn Fn() -> Nonce> {
+    return Box::new(move || Nonce::new(nonce.to_string()));
+}
+
+#[cfg(debug_assertions)]
+fn systemtime_strftime<T>(dt: T, format: &str) -> String
+   where T: Into<OffsetDateTime>
+{
+    let f = format_description::parse(format);
+    match f {
+        Ok(f) => {
+            match dt.into().format(&f) {
+                Ok(s) => s,
+                Err(_e) => "".to_string(),
+            }
+        },
+        Err(_e) => {
+            "".to_string()
+        },
+    }
+}
+
+impl ZeroIDC {
+    pub fn new(
+        issuer: &str,
+        client_id: &str,
+        auth_ep: &str,
+        local_web_port: u16,
+    ) -> Result<ZeroIDC, ZeroIDCError> {
+        let idc = ZeroIDC {
+            inner: Arc::new(Mutex::new(Inner {
+                running: false,
+                auth_endpoint: auth_ep.to_string(),
+                oidc_thread: None,
+                oidc_client: None,
+                access_token: None,
+                refresh_token: None,
+                exp_time: 0,
+
+                url: None,
+                csrf_token: None,
+                nonce: None,
+                pkce_verifier: None, 
+            })),
+        };
+
+        let iss = IssuerUrl::new(issuer.to_string())?;
+
+        let provider_meta = CoreProviderMetadata::discover(&iss, http_client)?;
+
+        let r = format!("http://localhost:{}/sso", local_web_port);
+        let redir_url = Url::parse(&r)?;
+
+        let redirect = RedirectUrl::new(redir_url.to_string())?;
+
+        (*idc.inner.lock().unwrap()).oidc_client = Some(
+            CoreClient::from_provider_metadata(
+                provider_meta,
+                ClientId::new(client_id.to_string()),
+                None,
+            )
+            .set_redirect_uri(redirect),
+        );
+
+        Ok(idc)
+    }
+
+    fn start(&mut self) {
+        let local = Arc::clone(&self.inner);
+
+        if !(*local.lock().unwrap()).running {
+            let inner_local = Arc::clone(&self.inner);
+            (*local.lock().unwrap()).oidc_thread = Some(spawn(move || {
+                (*inner_local.lock().unwrap()).running = true;
+                let mut running = true;
+
+                // Keep a copy of the initial nonce used to get the tokens
+                // Will be needed later when verifying the responses from refresh tokens
+                let nonce = (*inner_local.lock().unwrap()).nonce.clone();
+
+                while running {
+                    let exp = UNIX_EPOCH + Duration::from_secs((*inner_local.lock().unwrap()).exp_time);
+                    let now = SystemTime::now();
+
+                    #[cfg(debug_assertions)] {
+                        println!("refresh token thread tick, now: {}, exp: {}", systemtime_strftime(now, "[year]-[month]-[day] [hour]:[minute]:[second]"), systemtime_strftime(exp, "[year]-[month]-[day] [hour]:[minute]:[second]"));
+                    }
+                    let refresh_token = (*inner_local.lock().unwrap()).refresh_token.clone();
+                    if let Some(refresh_token) =  refresh_token {
+                        if now >= (exp - Duration::from_secs(30)) {
+                            let token_response = (*inner_local.lock().unwrap()).oidc_client.as_ref().map(|c| {
+                                let res = c.exchange_refresh_token(&refresh_token)
+                                    .request(http_client);
+                                
+                                res
+                            });
+                            
+                            if let Some(res) = token_response {
+                                match res {
+                                    Ok(res) => {
+
+                                        let n = match nonce.clone() {
+                                            Some(n) => n,
+                                            None => {
+                                                println!("err: no nonce");
+                                                continue;
+                                            }
+                                        };
+                                        
+                                        let id = match res.id_token() {
+                                            Some(t) => t,
+                                            None => {
+                                                println!("err: no id_token");
+                                                continue;
+                                            }
+                                        };
+            
+                                        // verify & validate claims
+                                        let verified = (*inner_local.lock().unwrap()).oidc_client.as_ref().map(|c| {
+                                            let claims = match id.claims(&c.id_token_verifier(), &n) {
+                                                Ok(c) => c,
+                                                Err(e) => {
+                                                    println!("claims err: {}", e);
+                                                    return false;
+                                                }
+                                            };
+                
+                                            let signing_algo = match id.signing_alg() {
+                                                Ok(s) => s,
+                                                Err(e) => {
+                                                    println!("alg err: {}", e);
+                                                    return false;
+                                                }
+                                            };
+                
+                                            if let Some(expected_hash) = claims.access_token_hash() {
+                                                let actual_hash = match AccessTokenHash::from_token(res.access_token(), &signing_algo) {
+                                                    Ok(h) => h,
+                                                    Err(e) => {
+                                                        println!("Error hashing access token: {}", e);
+                                                        return false;
+                                                    }
+                                                };
+                
+                                                if actual_hash != *expected_hash {
+                                                    println!("token hash error");
+                                                    return false;
+                                                }
+                                            }
+                                            return true;
+                                        });
+                                        
+                                        let v = match verified {
+                                            Some(verified) => {
+                                                if !verified {
+                                                    println!("not verified.");
+                                                    (*inner_local.lock().unwrap()).running = false;
+                                                    false
+                                                } else {
+                                                    true
+                                                }
+                                            },
+                                            None => {
+                                                println!("no verification performed?");
+                                                (*inner_local.lock().unwrap()).running = false;
+                                                false
+                                            }
+                                        };
+                                        
+                                        if v {
+                                            match res.id_token() {
+                                                Some(id_token) => {
+                                                    let params = [("id_token", id_token.to_string()),("state", "refresh".to_string())];
+                                                    #[cfg(debug_assertions)] {
+                                                        println!("New ID token: {}", id_token.to_string());
+                                                    }
+                                                    let client = reqwest::blocking::Client::new();
+                                                    let r = client.post((*inner_local.lock().unwrap()).auth_endpoint.clone())
+                                                        .form(&params)
+                                                        .send();
+
+                                                    match r {
+                                                        Ok(r) => {
+                                                            if r.status().is_success() {
+                                                                #[cfg(debug_assertions)] {
+                                                                    println!("hit url: {}", r.url().as_str());
+                                                                    println!("status: {}", r.status());
+                                                                }
+
+                                                                let access_token = res.access_token();
+                                                                let at = access_token.secret();
+                                                                // yes this function is called `dangerous_insecure_decode`
+                                                                // and it doesn't validate the jwt token signature, 
+                                                                // but if we've gotten this far, our claims have already
+                                                                // been validated up above
+                                                                let exp = dangerous_insecure_decode::<Exp>(&at);
+                                                                
+                                                                if let Ok(e) = exp {
+                                                                    (*inner_local.lock().unwrap()).exp_time = e.claims.exp
+                                                                }
+
+                                                                (*inner_local.lock().unwrap()).access_token = Some(access_token.clone());
+                                                                if let Some(t) = res.refresh_token() {
+                                                                    // println!("New Refresh Token: {}", t.secret());
+                                                                    (*inner_local.lock().unwrap()).refresh_token = Some(t.clone());
+                                                                }
+                                                                #[cfg(debug_assertions)] {
+                                                                    println!("Central post succeeded");
+                                                                }
+                                                            } else {
+                                                                println!("Central post failed: {}", r.status().to_string());
+                                                                println!("hit url: {}", r.url().as_str());
+                                                                println!("Status: {}", r.status());
+                                                                (*inner_local.lock().unwrap()).exp_time = 0;
+                                                                (*inner_local.lock().unwrap()).running = false;
+                                                            }
+                                                        },
+                                                        Err(e) => {
+                                                            
+                                                            println!("Central post failed: {}", e.to_string());
+                                                            println!("hit url: {}", e.url().unwrap().as_str());
+                                                            println!("Status: {}", e.status().unwrap());
+                                                            (*inner_local.lock().unwrap()).exp_time = 0;
+                                                            (*inner_local.lock().unwrap()).running = false;
+                                                        }
+                                                    }
+                                                },
+                                                None => {
+                                                    println!("no id token?!?");
+                                                }
+                                            }
+                                        } else {
+                                            println!("claims not verified");
+                                        }
+                                    },
+                                    Err(e) => {
+                                        println!("token error: {}", e);
+                                    }
+                                }
+                            } else {
+                                println!("token response??");
+                            }
+                        } else {
+                            #[cfg(debug_assertions)]
+                            println!("waiting to refresh");
+                        }
+                    } else {
+                        println!("no refresh token?");
+                    }
+
+                    sleep(Duration::from_secs(1));
+                    running = (*inner_local.lock().unwrap()).running;
+                }
+
+                println!("thread done!")
+            }));
+        }
+    }
+
+    pub fn stop(&mut self) {
+        let local = self.inner.clone();
+        if (*local.lock().unwrap()).running {
+            if let Some(u) = (*local.lock().unwrap()).oidc_thread.take() {
+                u.join().expect("join failed");
+            }
+        }
+    }
+
+    pub fn is_running(&mut self) -> bool {
+        let local = Arc::clone(&self.inner);
+
+        if (*local.lock().unwrap()).running {
+            true
+        } else {
+            false
+        }
+    }
+
+    pub fn get_exp_time(&mut self) -> u64 {
+        return (*self.inner.lock().unwrap()).exp_time;
+    }
+
+    pub fn set_nonce_and_csrf(&mut self, csrf_token: String, nonce: String) {
+        let local = Arc::clone(&self.inner);
+        (*local.lock().expect("can't lock inner")).as_opt().map(|i| {
+            let need_verifier = match i.pkce_verifier {
+                None => true,
+                _ => false,
+            };
+
+            let csrf_diff = if let Some(csrf) = i.csrf_token.clone() {
+                if *csrf.secret() != csrf_token {
+                    true    
+                } else {
+                    false
+                }
+            } else {
+                false
+            };
+
+            let nonce_diff = if let Some(n) = i.nonce.clone() {
+                if *n.secret() != nonce {
+                    true
+                } else {
+                    false
+                }
+            } else {
+                false
+            };
+
+            if need_verifier || csrf_diff || nonce_diff {
+                let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
+                let r = i.oidc_client.as_ref().map(|c| {
+                    let (auth_url, csrf_token, nonce) = c
+                    .authorize_url(
+                        AuthenticationFlow::<CoreResponseType>::AuthorizationCode,
+                        csrf_func(csrf_token),
+                        nonce_func(nonce),
+                    )
+                    .add_scope(Scope::new("profile".to_string()))
+                    .add_scope(Scope::new("email".to_string()))
+                    .add_scope(Scope::new("offline_access".to_string()))
+                    .add_scope(Scope::new("openid".to_string()))
+                    .set_pkce_challenge(pkce_challenge)
+                    .url();
+
+                    (auth_url, csrf_token, nonce)
+                });
+
+                if let Some(r) = r {
+                    i.url = Some(r.0);
+                    i.csrf_token = Some(r.1);
+                    i.nonce = Some(r.2);
+                    i.pkce_verifier = Some(pkce_verifier);
+                }
+            }
+        });
+    }
+
+    pub fn auth_url(&self) -> String {
+        let url = (*self.inner.lock().expect("can't lock inner")).as_opt().map(|i| {
+            match i.url.clone() {
+                Some(u) => u.to_string(),
+                _ => "".to_string(),
+            }
+        });
+
+        match url {
+            Some(url) => url.to_string(),
+            None => "".to_string(),
+        }
+    }
+
+    pub fn do_token_exchange(&mut self, code: &str) -> String {
+        let local = Arc::clone(&self.inner);
+        let mut should_start = false;
+        let res = (*local.lock().unwrap()).as_opt().map(|i| {
+            if let Some(verifier) = i.pkce_verifier.take() {
+                let token_response = i.oidc_client.as_ref().map(|c| {
+                    let r = c.exchange_code(AuthorizationCode::new(code.to_string()))
+                        .set_pkce_verifier(verifier)
+                        .request(http_client);
+
+                    // validate the token hashes
+                    match r {
+                        Ok(res) =>{
+                            let n = match i.nonce.clone() {
+                                Some(n) => n,
+                                None => {
+                                    return None;
+                                }
+                            };
+                            
+                            let id = match res.id_token() {
+                                Some(t) => t,
+                                None => {
+                                    return None;
+                                }
+                            };
+
+                            let claims = match id.claims(&c.id_token_verifier(), &n) {
+                                Ok(c) => c,
+                                Err(_e) => {
+                                    return None;
+                                }
+                            };
+
+                            let signing_algo = match id.signing_alg() {
+                                Ok(s) => s,
+                                Err(_) => {
+                                    return None;
+                                }
+                            };
+
+                            if let Some(expected_hash) = claims.access_token_hash() {
+                                let actual_hash = match AccessTokenHash::from_token(res.access_token(), &signing_algo) {
+                                    Ok(h) => h,
+                                    Err(e) => {
+                                        println!("Error hashing access token: {}", e);
+                                        return None;
+                                    }
+                                };
+
+                                if actual_hash != *expected_hash {
+                                    println!("token hash error");
+                                    return None;
+                                }
+                            }
+                            Some(res)
+                        },
+                        Err(_e) => {
+                            #[cfg(debug_assertions)] {
+                                println!("token response error: {}", _e.to_string());
+                            }
+
+                            return None;
+                        },
+                    }
+                });
+                
+                if let Some(Some(tok)) = token_response {
+                    let id_token = tok.id_token().unwrap();
+                    #[cfg(debug_assertions)] {
+                        println!("ID token: {}", id_token.to_string());
+                    }
+
+                    let mut split = "".to_string();
+                    match i.csrf_token.clone() {
+                        Some(csrf_token) => {
+                            split = csrf_token.secret().to_owned();
+                        },
+                        _ => (),
+                    }
+
+                    let split = split.split("_").collect::<Vec<&str>>();
+                    
+                    if split.len() == 2 {
+                        let params = [("id_token", id_token.to_string()),("state", split[0].to_string())];
+                        let client = reqwest::blocking::Client::new();
+                        let res = client.post(i.auth_endpoint.clone())
+                            .form(&params)
+                            .send();
+
+                        match res {
+                            Ok(res) => {
+                                #[cfg(debug_assertions)] {
+                                    println!("hit url: {}", res.url().as_str());
+                                    println!("Status: {}", res.status());
+                                }
+
+                                let at = tok.access_token().secret();
+
+                                // see previous note about this function's use
+                                let exp = dangerous_insecure_decode::<Exp>(&at);
+                                if let Ok(e) = exp {
+                                    i.exp_time = e.claims.exp
+                                }
+
+                                i.access_token = Some(tok.access_token().clone());
+                                if let Some(t) = tok.refresh_token() {
+                                    i.refresh_token = Some(t.clone());
+                                    should_start = true;
+                                }
+                                #[cfg(debug_assertions)] {
+                                    let access_token = tok.access_token();
+                                    println!("Access Token: {}", access_token.secret());
+
+                                    let refresh_token = tok.refresh_token();
+                                    println!("Refresh Token: {}", refresh_token.unwrap().secret());
+                                }
+                        
+                                let bytes = match res.bytes() {
+                                    Ok(bytes) => bytes,
+                                    Err(_) => Bytes::from(""),
+                                };
+
+                                let bytes = match from_utf8(bytes.as_ref()) {
+                                    Ok(bytes) => bytes.to_string(),
+                                    Err(_) => "".to_string(),
+                                };
+
+                                return bytes;
+                            },
+                            Err(res) => {
+                                println!("hit url: {}", res.url().unwrap().as_str());
+                                println!("Status: {}", res.status().unwrap());
+                                println!("Post error: {}", res.to_string());
+                                i.exp_time = 0;
+                            }
+                        }
+
+                        
+                    } else {
+                        println!("invalid split length?!?");
+                    }
+                }
+            }
+            "".to_string()
+        });
+        if should_start {
+            self.start();
+        }
+        return match res {
+            Some(res) => res,
+            _ => "".to_string(),
+        };
+    }
+}
+

+ 109 - 0
zeroidc/zeroidc.vcxproj

@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ItemGroup Label="ProjectConfigurations">
+    <ProjectConfiguration Include="Debug|Win32">
+      <Configuration>Debug</Configuration>
+      <Platform>Win32</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Release|Win32">
+      <Configuration>Release</Configuration>
+      <Platform>Win32</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Debug|x64">
+      <Configuration>Debug</Configuration>
+      <Platform>x64</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Release|x64">
+      <Configuration>Release</Configuration>
+      <Platform>x64</Platform>
+    </ProjectConfiguration>
+  </ItemGroup>
+  <PropertyGroup Label="Globals">
+    <VCProjectVersion>16.0</VCProjectVersion>
+    <ProjectGuid>{175C340F-F5BA-4CB1-88AD-533B102E3799}</ProjectGuid>
+    <Keyword>Win32Proj</Keyword>
+  </PropertyGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
+    <ConfigurationType>Makefile</ConfigurationType>
+    <UseDebugLibraries>true</UseDebugLibraries>
+    <PlatformToolset>v142</PlatformToolset>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
+    <ConfigurationType>Makefile</ConfigurationType>
+    <UseDebugLibraries>false</UseDebugLibraries>
+    <PlatformToolset>v142</PlatformToolset>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
+    <ConfigurationType>Makefile</ConfigurationType>
+    <UseDebugLibraries>true</UseDebugLibraries>
+    <PlatformToolset>v142</PlatformToolset>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
+    <ConfigurationType>Makefile</ConfigurationType>
+    <UseDebugLibraries>false</UseDebugLibraries>
+    <PlatformToolset>v142</PlatformToolset>
+  </PropertyGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
+  <ImportGroup Label="ExtensionSettings">
+  </ImportGroup>
+  <ImportGroup Label="Shared">
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+  </ImportGroup>
+  <PropertyGroup Label="UserMacros" />
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
+    <NMakeBuildCommandLine>cargo build --release --target=x86_64-pc-windows-msvc</NMakeBuildCommandLine>
+    <NMakeOutput>
+    </NMakeOutput>
+    <NMakeCleanCommandLine>cargo clean</NMakeCleanCommandLine>
+    <NMakeReBuildCommandLine>cargo clean &amp; cargo build --release --target=x86_64-pc-windows-msvc</NMakeReBuildCommandLine>
+    <NMakePreprocessorDefinitions>NDEBUG;$(NMakePreprocessorDefinitions)</NMakePreprocessorDefinitions>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+    <NMakeBuildCommandLine>cargo build --target=i686-pc-windows-msvc</NMakeBuildCommandLine>
+    <NMakeOutput>
+    </NMakeOutput>
+    <NMakeCleanCommandLine>cargo clean</NMakeCleanCommandLine>
+    <NMakeReBuildCommandLine>cargo clean &amp; cargo build --target=i686-pc-windows-msvc</NMakeReBuildCommandLine>
+    <NMakePreprocessorDefinitions>WIN32;_DEBUG;$(NMakePreprocessorDefinitions)</NMakePreprocessorDefinitions>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+    <NMakeBuildCommandLine>cargo build --target=x86_64-pc-windows-msvc</NMakeBuildCommandLine>
+    <NMakeOutput>
+    </NMakeOutput>
+    <NMakeCleanCommandLine>cargo clean</NMakeCleanCommandLine>
+    <NMakeReBuildCommandLine>cargo clean &amp; cargo build --target=x86_64-pc-windows-msvc</NMakeReBuildCommandLine>
+    <NMakePreprocessorDefinitions>_DEBUG;$(NMakePreprocessorDefinitions)</NMakePreprocessorDefinitions>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <NMakeBuildCommandLine>cargo build --release --target=i686-pc-windows-msvc</NMakeBuildCommandLine>
+    <NMakeOutput>
+    </NMakeOutput>
+    <NMakeCleanCommandLine>cargo clean</NMakeCleanCommandLine>
+    <NMakeReBuildCommandLine>cargo clean &amp; cargo build --release --target=i686-pc-windows-msvc</NMakeReBuildCommandLine>
+    <NMakePreprocessorDefinitions>WIN32;NDEBUG;$(NMakePreprocessorDefinitions)</NMakePreprocessorDefinitions>
+  </PropertyGroup>
+  <ItemDefinitionGroup>
+  </ItemDefinitionGroup>
+  <ItemGroup>
+    <None Include="src\ext.rs" />
+    <None Include="src\lib.rs" />
+  </ItemGroup>
+  <ItemGroup>
+    <ClInclude Include="target\zeroidc.h" />
+  </ItemGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
+  <ImportGroup Label="ExtensionTargets">
+  </ImportGroup>
+</Project>

+ 30 - 0
zeroidc/zeroidc.vcxproj.filters

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ItemGroup>
+    <Filter Include="Source Files">
+      <UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
+      <Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
+    </Filter>
+    <Filter Include="Header Files">
+      <UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
+      <Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
+    </Filter>
+    <Filter Include="Resource Files">
+      <UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
+      <Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
+    </Filter>
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="src\ext.rs">
+      <Filter>Source Files</Filter>
+    </None>
+    <None Include="src\lib.rs">
+      <Filter>Source Files</Filter>
+    </None>
+  </ItemGroup>
+  <ItemGroup>
+    <ClInclude Include="target\zeroidc.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+  </ItemGroup>
+</Project>