From 2c682b4d1cdfd64d3a5b931bd0a67abb1f8b731e Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Wed, 9 Aug 2017 14:37:19 -0700 Subject: [PATCH] Small controller revisions, first run of controller API model JavaScript. --- controller/EmbeddedNetworkController.cpp | 101 ++--- controller/EmbeddedNetworkController.hpp | 5 +- controller/controller-api-model.js | 546 +++++++++++++++++++++++ 3 files changed, 577 insertions(+), 75 deletions(-) create mode 100644 controller/controller-api-model.js diff --git a/controller/EmbeddedNetworkController.cpp b/controller/EmbeddedNetworkController.cpp index 72d476224..764b5c201 100644 --- a/controller/EmbeddedNetworkController.cpp +++ b/controller/EmbeddedNetworkController.cpp @@ -638,14 +638,10 @@ unsigned int EmbeddedNetworkController::handleControlPlaneHttpPOST( if (newAuth != OSUtils::jsonBool(member["authorized"],false)) { member["authorized"] = newAuth; member[((newAuth) ? "lastAuthorizedTime" : "lastDeauthorizedTime")] = now; - - json ah; - ah["a"] = newAuth; - ah["by"] = "api"; - ah["ts"] = now; - ah["ct"] = json(); - ah["c"] = json(); - member["authHistory"].push_back(ah); + if (newAuth) { + member["lastAuthorizedCredentialType"] = "api"; + member["lastAuthorizedCredential"] = json(); + } // Member is being de-authorized, so spray Revocation objects to all online members if (!newAuth) { @@ -896,22 +892,15 @@ unsigned int EmbeddedNetworkController::handleControlPlaneHttpPOST( if (b.count("authTokens")) { json &authTokens = b["authTokens"]; - if (authTokens.is_array()) { - json nat = json::array(); - for(unsigned long i=0;i 0) { - json t = json::object(); - t["token"] = tstr; - t["expires"] = OSUtils::jsonInt(token["expires"],0ULL); - t["maxUsesPerMember"] = OSUtils::jsonInt(token["maxUsesPerMember"],0ULL); - nat.push_back(t); - } - } + if (authTokens.is_object()) { + json nat; + for(json::iterator t(authTokens.begin());t!=authTokens.end();++t) { + if ((t.value().is_number())&&(t.value() >= 0)) + nat[t.key()] = t.value(); } network["authTokens"] = nat; + } else { + network["authTokens"] = {{}}; } } @@ -1268,56 +1257,29 @@ void EmbeddedNetworkController::_request( } // Determine whether and how member is authorized - const char *authorizedBy = (const char *)0; + bool authorized = false; bool autoAuthorized = false; json autoAuthCredentialType,autoAuthCredential; if (OSUtils::jsonBool(member["authorized"],false)) { - authorizedBy = "memberIsAuthorized"; + authorized = true; } else if (!OSUtils::jsonBool(network["private"],true)) { - authorizedBy = "networkIsPublic"; - json &ahist = member["authHistory"]; - if ((!ahist.is_array())||(ahist.size() == 0)) - autoAuthorized = true; + authorized = true; + autoAuthorized = true; + autoAuthCredentialType = "public"; } else { char presentedAuth[512]; if (metaData.get(ZT_NETWORKCONFIG_REQUEST_METADATA_KEY_AUTH,presentedAuth,sizeof(presentedAuth)) > 0) { presentedAuth[511] = (char)0; // sanity check - - // Check for bearer token presented by member if ((strlen(presentedAuth) > 6)&&(!strncmp(presentedAuth,"token:",6))) { const char *const presentedToken = presentedAuth + 6; - - json &authTokens = network["authTokens"]; - if (authTokens.is_array()) { - for(unsigned long i=0;i now))&&(tstr == presentedToken)) { - bool usable = (maxUses == 0); - if (!usable) { - uint64_t useCount = 0; - json &ahist = member["authHistory"]; - if (ahist.is_array()) { - for(unsigned long j=0;j now)) { + authorized = true; + autoAuthorized = true; + autoAuthCredentialType = "token"; + autoAuthCredential = presentedToken; } } } @@ -1325,23 +1287,16 @@ void EmbeddedNetworkController::_request( } // If we auto-authorized, update member record - if ((autoAuthorized)&&(authorizedBy)) { + if ((autoAuthorized)&&(authorized)) { member["authorized"] = true; member["lastAuthorizedTime"] = now; - - json ah; - ah["a"] = true; - ah["by"] = authorizedBy; - ah["ts"] = now; - ah["ct"] = autoAuthCredentialType; - ah["c"] = autoAuthCredential; - member["authHistory"].push_back(ah); - + member["lastAuthorizedCredentialType"] = autoAuthCredentialType; + member["lastAuthorizedCredential"] = autoAuthCredential; json &revj = member["revision"]; member["revision"] = (revj.is_number() ? ((uint64_t)revj + 1ULL) : 1ULL); } - if (authorizedBy) { + if (authorized) { // Update version info and meta-data if authorized and if this is a genuine request if (requestPacketId) { const uint64_t vMajor = metaData.getUI(ZT_NETWORKCONFIG_REQUEST_METADATA_KEY_NODE_MAJOR_VERSION,0); diff --git a/controller/EmbeddedNetworkController.hpp b/controller/EmbeddedNetworkController.hpp index 8752922ed..590a8b48f 100644 --- a/controller/EmbeddedNetworkController.hpp +++ b/controller/EmbeddedNetworkController.hpp @@ -129,7 +129,6 @@ private: inline void _initMember(nlohmann::json &member) { if (!member.count("authorized")) member["authorized"] = false; - if (!member.count("authHistory")) member["authHistory"] = nlohmann::json::array(); if (!member.count("ipAssignments")) member["ipAssignments"] = nlohmann::json::array(); if (!member.count("activeBridge")) member["activeBridge"] = false; if (!member.count("tags")) member["tags"] = nlohmann::json::array(); @@ -139,6 +138,8 @@ private: if (!member.count("revision")) member["revision"] = 0ULL; if (!member.count("lastDeauthorizedTime")) member["lastDeauthorizedTime"] = 0ULL; if (!member.count("lastAuthorizedTime")) member["lastAuthorizedTime"] = 0ULL; + if (!member.count("lastAuthorizedCredentialType")) member["lastAuthorizedCredentialType"] = nlohmann::json(); + if (!member.count("lastAuthorizedCredential")) member["lastAuthorizedCredential"] = nlohmann::json(); if (!member.count("vMajor")) member["vMajor"] = -1; if (!member.count("vMinor")) member["vMinor"] = -1; if (!member.count("vRev")) member["vRev"] = -1; @@ -156,7 +157,7 @@ private: if (!network.count("enableBroadcast")) network["enableBroadcast"] = true; if (!network.count("v4AssignMode")) network["v4AssignMode"] = {{"zt",false}}; if (!network.count("v6AssignMode")) network["v6AssignMode"] = {{"rfc4193",false},{"zt",false},{"6plane",false}}; - if (!network.count("authTokens")) network["authTokens"] = nlohmann::json::array(); + if (!network.count("authTokens")) network["authTokens"] = {{}}; if (!network.count("capabilities")) network["capabilities"] = nlohmann::json::array(); if (!network.count("tags")) network["tags"] = nlohmann::json::array(); if (!network.count("routes")) network["routes"] = nlohmann::json::array(); diff --git a/controller/controller-api-model.js b/controller/controller-api-model.js new file mode 100644 index 000000000..7b61dff4e --- /dev/null +++ b/controller/controller-api-model.js @@ -0,0 +1,546 @@ +/* + * A JavaScript class based model for the ZeroTier controller microservice API + * Copyright (C) 2011-2017 ZeroTier, Inc. https://www.zerotier.com/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * -- + * + * You can be released from the requirements of the license by purchasing + * a commercial license. Buying such a license is mandatory as soon as you + * develop commercial closed-source software that incorporates or links + * directly against ZeroTier software without disclosing the source code + * of your own application. + */ + +'use strict'; + +/** + * Goes through a rule set array and makes sure it's valid, returning a canonicalized version + * + * @param {array[object]} rules Array of ZeroTier rules + * @return New array of canonicalized rules + * @throws {Error} Rule set is invalid + */ +function formatRuleSetArray(rules) +{ +} +exports.formatRuleSetArray = formatRuleSetArray; + +/** + * @param {string} IP with optional /netmask|port section + * @return 4, 6, or 0 if invalid + */ +function ipClassify(ip) +{ + if ((!ip)||(typeof ip !== 'string')) + return 0; + let ips = ip.split('/'); + if (ips.length > 0) { + if (ips.length > 1) { + if (ips[1].length === 0) + return 0; + for(let i=0;i 0) { + for(let i=0;i 0) { + for(let i=0;i= 0) { + r += c; + if (r.length === l) + break; + } + } + + while (r.length < l) + r = '0' + r; + + return r; +}; +exports.formatZeroTierIdentifier = formatZeroTierIdentifier; + +// Internal container classes +class _V4AssignMode +{ + get zt() { return (this._zt)||false; } + set zt(b) { this._zt = !!b; } + toJSON() + { + return { zt: this.zt }; + } +}; +class _v6AssignMode +{ + get ['6plane'] { return (this._6plane)||false; } + set ['6plane'](b) { this._6plane = !!b; } + get zt() { return (this._zt)||false; } + set zt(b) { this._zt = !!b; } + get rfc4193() { return (this._rfc4193)||false; } + set rfc4193(b) { this._rfc4193 = !!b; } + toJSON() + { + return { + zt: this.zt, + rfc4193: this.rfc4193, + '6plane': this['6plane'] + }; + } +} + +class Network +{ + constructor(obj) + { + this.clear(); + this.patch(obj); + } + + get objtype() { return 'network'; } + + get id() { return this._id; } + set id(x) { return (this._id = formatZeroTierIdentifier(x,16)); } + + get nwid() { return this._id; } // legacy + + get authTokens() { return this._authTokens; } + set authTokens(at) + { + this._authTokens = {}; + if ((at)&&(typeof at === 'object')&&(!Array.isArray(at))) { + for(let k in at) { + let exp = parseInt(at[k])||0; + if (exp >= 0) + this._authTokens[k] = exp; + } + } + return this._authTokens; + } + + get capabilities() { return this._capabilities; } + set capabilities(c) + { + let ca = []; + let ids = {}; + if ((c)&&(Array.isArray(c))) { + for(let a=0;a= 0)&&(capId <= 0xffffffff)&&(!ids[capId])) { + ids[capId] = true; + let capDefault = !!cap['default']; + let capRules = formatRuleSetArray(cap.rules); + ca.push({ + id: capId, + 'default': capDefault, + rules: capRules + }); + } + } + } + } + ca.sort(function(a,b) { + a = a.id; + b = b.id; + return ((a > b) ? 1 : ((a < b) ? -1 : 0)); + }); + this._capabilities = ca; + return ca; + } + + get ipAssignmentPools() return { this._ipAssignmentPools; } + set ipAssignmentPools(ipp) + { + let pa = []; + let ranges = {}; + if ((ipp)&&(Array.isArray(ipp))) { + for(let a=0;a 0)&&(stype === ipClassify(end))&&(!ranges[start+'_'+end])) { + ranges[start+'_'+end] = true; + pa.push({ ipRangeStart: start, ipRangeEnd: end }); + } + } + } + } + } + pa.sort(function(a,b) { return a.ipRangeStart.localeCompare(b.ipRangeStart); }); + this._ipAssignmentPools = pa; + return pa; + } + + get multicastLimit() return { this._multicastLimit; } + set multicastLimit(n) + { + try { + let nn = parseInt(n)||0; + this._multicastLimit = (nn >= 0) ? nn : 0; + } catch (e) { + this._multicastLimit = 0; + } + return this._multicastLimit; + } + + get routes() return { this._routes; } + set routes(r) + { + let ra = []; + let targets = {}; + if ((r)&&(Array.isArray(r))) { + for(let a=0;a 0)&&((routeVia === null)||(ipClassify(routeVia) === rtt))&&(!targets[routeTarget])) { + targets[routeTarget] = true; + ra.push({ target: routeTarget, via: routeVia }); + } + } + } + } + ra.sort(function(a,b) { return a.routeTarget.localeCompare(b.routeTarget); }); + this._routes = ra; + return ra; + } + + get tags() return { this._tags; } + set tags(t) + { + let ta = []; + if ((t)&&(Array.isArray(t))) { + for(let a=0;a= 0)||(tagId <= 0xffffffff)) { + let tagDefault = tag.default; + if (typeof tagDefault !== 'number') + tagDefault = parseInt(tagDefault)||null; + if ((tagDefault < 0)||(tagDefault > 0xffffffff)) + tagDefault = null; + ta.push({ 'id': tagId, 'default': tagDefault }); + } + } + } + } + ta.sort(function(a,b) { + a = a.id; + b = b.id; + return ((a > b) ? 1 : ((a < b) ? -1 : 0)); + }); + this._tags = ta; + return ta; + } + + get v4AssignMode() return { this._v4AssignMode; } + set v4AssignMode(m) + { + if ((m)&&(typeof m === 'object')&&(!Array.isArray(m))) { + this._v4AssignMode.zt = m.zt; + } else if (m === 'zt') { // legacy + this._v4AssignMode.zt = true; + } else { + this._v4AssignMode.zt = false; + } + } + + get v6AssignMode() return { this._v6AssignMode; } + set v6AssignMode(m) + { + if ((m)&&(typeof m === 'object')&&(!Array.isArray(m))) { + this._v6AssignMode.zt = m.zt; + this._v6AssignMode.rfc4193 = m.rfc4193; + this._v6AssignMode['6plane'] = m['6plane']; + } else if (typeof m === 'string') { // legacy + let ms = m.split(','); + this._v6AssignMode.zt = false; + this._v6AssignMode.rfc4193 = false; + this._v6AssignMode['6plane'] = false; + for(let i=0;i= 10000) mtu = 10000; // maximum as per ZT spec + this._mtu = mtu; + } + + get name() { return this._name; } + set name(n) + { + if (typeof n === 'string') + this._name = n; + else if (typeof n === 'number') + this._name = n.toString(); + else this._name = ''; + } + + get private() { return this._private; } + set private(b) + { + // This is really meaningful for security, so make true unless explicitly set to false. + this._private = (b !== false); + } + + get activeMemberCount() { return this.__activeMemberCount; } + get authorizedMemberCount() { return this.__authorizedMemberCount; } + get totalMemberCount() { return this.__totalMemberCount; } + get clock() { return this.__clock; } + get creationTime() { return this.__creationTime; } + get revision() { return this.__revision; } + + toJSONExcludeControllerGenerated() + { + return { + id: this.id, + objtype: 'network', + nwid: this.nwid, + authTokens: this.authTokens, + capabilities: this.capabilities, + ipAssignmentPools: this.ipAssignmentPools, + multicastLimit: this.multicastLimit, + routes: this.routes, + tags: this.tags, + v4AssignMode: this._v4AssignMode.toJSON(), + v6AssignMode: this._v6AssignMode.toJSON(), + rules: this.rules, + enableBroadcast: this.enableBroadcast, + mtu: this.mtu, + name: this.name, + 'private': this['private'] + }; + } + + toJSON() + { + var j = this.toJSONExcludeControllerGenerated(); + j.activeMemberCount = this.activeMemberCount; + j.authorizedMemberCount = this.authorizedMemberCount; + j.totalMemberCount = this.totalMemberCount; + j.clock = this.clock; + j.creationTime = this.creationTime; + j.revision = this.revision; + return j; + } + + clear() + { + this._id = ''; + this._authTokens = {}; + this._capabilities = []; + this._ipAssignmentPools = []; + this._multicastLimit = 32; + this._routes = []; + this._tags = []; + this._v4AssignMode = new _V4AssignMode(); + this._v6AssignMode = new _v6AssignMode(); + this._rules = []; + this._enableBroadcast = true; + this._mtu = 2800; + this._name = ''; + this._private = true; + + this.__activeMemberCount = 0; + this.__authorizedMemberCount = 0; + this.__totalMemberCount = 0; + this.__clock = 0; + this.__creationTime = 0; + this.__revision = 0; + } + + patch(obj) + { + if (obj instanceof Network) + obj = obj.toJSON(); + if ((obj)&&(typeof obj === 'object')&&(!Array.isArray(obj))) { + for(var k in obj) { + try { + switch(k) { + case 'id': + case 'authTokens': + case 'capabilities': + case 'ipAssignmentPools': + case 'multicastLimit': + case 'routes': + case 'tags': + case 'rules': + case 'enableBroadcast': + case 'mtu': + case 'name': + case 'private': + case 'v4AssignMode': + case 'v6AssignMode': + this[k] = obj[k]; + break; + + case 'activeMemberCount': + case 'authorizedMemberCount': + case 'totalMemberCount': + case 'clock': + case 'creationTime': + case 'revision': + this['__'+k] = parseInt(obj[k])||0; + break; + } + } catch (e) {} + } + } + } +}; +exports.Network = Network; + +class Member +{ + constructor(obj) + { + this.clear(); + this.patch(obj); + } + + get objtype() { return 'member'; } + + get id() { return this._id; } + set id(x) { this._id = formatZeroTierIdentifier((typeof x === 'number') ? x.toString(16) : x,10); } + + get address() { return this._id; } // legacy + + get nwid() { return this._nwid; } + set nwid(x) { this._nwid = formatZeroTierIdentifier(x,16); } + + get controllerId() { return this.nwid.substr(0,10); } + + get authorized() { return this._authorized; } + set authorized(b) { this._authorized = (b === true); } // security critical so require explicit set to true + + get activeBridge() { return this._activeBridge; } + set activeBridge(b) { this._activeBridge = !!b; } + + get capabilities() { return this._capabilities; } + set capabilities(c) + { + } + + get identity() { return this._identity; } + set identity(istr) + { + if ((istr)&&(typeof istr === 'string')) + this._identity = istr; + else this._identity = null; + } + + get ipAssignments() { return this._ipAssignments; } + set ipAssignments(ipa) + { + } + + get noAutoAssignIps() { return this._noAutoAssignIps; } + set noAutoAssignIps(b) { this._noAutoAssignIps = !!b; } + + get tags() { return this._tags; } + set tags(t) + { + } + + clear() + { + this._id = ''; + this._nwid = ''; + this._authorized = false; + this._activeBridge = false; + this._capabilities = []; + this._identity = ''; + this._ipAssignments = []; + this._noAutoAssignIps = false; + this._tags = []; + + this.__creationTime = 0; + this.__lastAuthorizedTime = 0; + this.__lastAuthorizedCredentialType = null; + this.__lastAuthorizedCredential = null; + this.__lastDeauthorizedTime = 0; + this.__physicalAddr = ''; + this.__revision = 0; + this.__vMajor = 0; + this.__vMinor = 0; + this.__vRev = 0; + this.__vProto = 0; + } +}; +exports.Member = Member;