2
0
Эх сурвалжийг харах

Basic controller JSON API seems to be working.

Adam Ierymenko 10 жил өмнө
parent
commit
69ceb7e730

+ 404 - 392
controller/SqliteNetworkController.cpp

@@ -559,422 +559,151 @@ unsigned int SqliteNetworkController::handleControlPlaneHttpGET(
 	std::string &responseBody,
 	std::string &responseContentType)
 {
-	char json[16384];
 	Mutex::Lock _l(_lock);
+	return _doCPGet(path,urlArgs,headers,body,responseBody,responseContentType);
+}
 
-	if ((path.size() > 0)&&(path[0] == "network")) {
+unsigned int SqliteNetworkController::handleControlPlaneHttpPOST(
+	const std::vector<std::string> &path,
+	const std::map<std::string,std::string> &urlArgs,
+	const std::map<std::string,std::string> &headers,
+	const std::string &body,
+	std::string &responseBody,
+	std::string &responseContentType)
+{
+	if (path.empty())
+		return 404;
+	Mutex::Lock _l(_lock);
+
+	if (path[0] == "network") {
 
 		if ((path.size() >= 2)&&(path[1].length() == 16)) {
 			uint64_t nwid = Utils::hexStrToU64(path[1].c_str());
 			char nwids[24];
 			Utils::snprintf(nwids,sizeof(nwids),"%.16llx",(unsigned long long)nwid);
 
+			int64_t revision = 0;
+			sqlite3_reset(_sGetNetworkRevision);
+			sqlite3_bind_text(_sGetNetworkRevision,1,nwids,16,SQLITE_STATIC);
+			bool networkExists = false;
+			if (sqlite3_step(_sGetNetworkRevision) == SQLITE_ROW) {
+				networkExists = true;
+				revision = sqlite3_column_int64(_sGetNetworkRevision,0);
+			}
+
 			if (path.size() >= 3) {
+
+				if (!networkExists)
+					return 404;
+
 				if ((path.size() == 4)&&(path[2] == "member")&&(path[3].length() == 10)) {
 					uint64_t address = Utils::hexStrToU64(path[3].c_str());
 					char addrs[24];
 					Utils::snprintf(addrs,sizeof(addrs),"%.10llx",address);
 
-					sqlite3_reset(_sGetMember2);
-					sqlite3_bind_text(_sGetMember2,1,nwids,16,SQLITE_STATIC);
-					sqlite3_bind_text(_sGetMember2,2,addrs,10,SQLITE_STATIC);
-					if (sqlite3_step(_sGetMember2) == SQLITE_ROW) {
-						Utils::snprintf(json,sizeof(json),
-							"{\n"
-							"\t\"nwid\": \"%s\",\n"
-							"\t\"address\": \"%s\",\n"
-							"\t\"authorized\": %s,\n"
-							"\t\"activeBridge\": %s,\n"
-							"\t\"lastAt\": \"%s\",\n"
-							"\t\"lastSeen\": %llu,\n"
-							"\t\"firstSeen\": %llu,\n"
-							"\t\"identity\": \"%s\",\n"
-							"\t\"ipAssignments\": [",
-							nwids,
-							addrs,
-							(sqlite3_column_int(_sGetMember2,0) > 0) ? "true" : "false",
-							(sqlite3_column_int(_sGetMember2,1) > 0) ? "true" : "false",
-							_jsonEscape((const char *)sqlite3_column_text(_sGetMember2,3)).c_str(),
-							(unsigned long long)sqlite3_column_int64(_sGetMember2,4),
-							(unsigned long long)sqlite3_column_int64(_sGetMember2,5),
-							_jsonEscape((const char *)sqlite3_column_text(_sGetMember2,2)).c_str());
-						responseBody = json;
+					int64_t memberRowId = 0;
+					sqlite3_reset(_sGetMember);
+					sqlite3_bind_text(_sGetMember,1,nwids,16,SQLITE_STATIC);
+					sqlite3_bind_text(_sGetMember,2,addrs,10,SQLITE_STATIC);
+					bool memberExists = false;
+					if (sqlite3_step(_sGetMember) == SQLITE_ROW) {
+						memberExists = true;
+						memberRowId = sqlite3_column_int64(_sGetMember,0);
+					}
 
-						sqlite3_reset(_sGetIpAssignmentsForNode2);
-						sqlite3_bind_text(_sGetIpAssignmentsForNode2,1,nwids,16,SQLITE_STATIC);
-						sqlite3_bind_text(_sGetIpAssignmentsForNode2,2,addrs,10,SQLITE_STATIC);
-						bool firstIp = true;
-						while (sqlite3_step(_sGetIpAssignmentPools2) == SQLITE_ROW) {
-							InetAddress ip((const void *)sqlite3_column_blob(_sGetIpAssignmentsForNode2,0),(sqlite3_column_int(_sGetIpAssignmentsForNode2,2) == 6) ? 16 : 4,(unsigned int)sqlite3_column_int(_sGetIpAssignmentPools2,1));
-							responseBody.append(firstIp ? "\"" : ",\"");
-							firstIp = false;
-							responseBody.append(_jsonEscape(ip.toString()));
-							responseBody.push_back('"');
-						}
+					if (!memberExists) {
+						sqlite3_reset(_sCreateMember);
+						sqlite3_bind_text(_sCreateMember,1,nwids,16,SQLITE_STATIC);
+						sqlite3_bind_text(_sCreateMember,2,addrs,10,SQLITE_STATIC);
+						sqlite3_bind_int(_sCreateMember,3,0);
+						if (sqlite3_step(_sCreateMember) != SQLITE_DONE)
+							return 500;
+						memberRowId = (int64_t)sqlite3_last_insert_rowid(_db);
+					}
 
-						responseBody.append("]");
+					json_value *j = json_parse(body.c_str(),body.length());
+					if (j) {
+						if (j->type == json_object) {
+							for(unsigned int k=0;k<j->u.object.length;++k) {
 
-						/* It's possible to get the actual netconf dictionary by including these
-						 * three URL arguments. The member identity must be the string
-						 * serialized identity of this member, and the signing identity must be
-						 * the full secret identity of this network controller. The have revision
-						 * is optional but would designate the revision our hypothetical client
-						 * already has.
-						 *
-						 * This is primarily for testing and is not used in production. It makes
-						 * it easy to test the entire network controller via its JSON API.
-						 *
-						 * If these arguments are included, three more object fields are returned:
-						 * 'netconf', 'netconfResult', and 'netconfResultMessage'. These are all
-						 * string fields and contain the actual netconf dictionary, the query
-						 * result code, and any verbose message e.g. an error description. */
-						std::map<std::string,std::string>::const_iterator memids(urlArgs.find("memberIdentity"));
-						std::map<std::string,std::string>::const_iterator sigids(urlArgs.find("signingIdentity"));
-						std::map<std::string,std::string>::const_iterator hrs(urlArgs.find("haveRevision"));
-						if ((memids != urlArgs.end())&&(sigids != urlArgs.end())) {
-							Dictionary netconf;
-							Identity memid,sigid;
-							try {
-								if (memid.fromString(memids->second)&&sigid.fromString(sigids->second)&&sigid.hasPrivate()) {
-									uint64_t hr = 0;
-									if (hrs != urlArgs.end())
-										hr = Utils::strToU64(hrs->second.c_str());
-									const char *result = "";
-									switch(this->doNetworkConfigRequest(InetAddress(),sigid,memid,nwid,Dictionary(),hr,netconf)) {
-										case NetworkController::NETCONF_QUERY_OK: result = "OK"; break;
-										case NetworkController::NETCONF_QUERY_OK_BUT_NOT_NEWER: result = "OK_BUT_NOT_NEWER"; break;
-										case NetworkController::NETCONF_QUERY_OBJECT_NOT_FOUND: result = "OBJECT_NOT_FOUND"; break;
-										case NetworkController::NETCONF_QUERY_ACCESS_DENIED: result = "ACCESS_DENIED"; break;
-										case NetworkController::NETCONF_QUERY_INTERNAL_SERVER_ERROR: result = "INTERNAL_SERVER_ERROR"; break;
-										default: result = "(unrecognized result code)"; break;
+								if (!strcmp(j->u.object.values[k].name,"authorized")) {
+									if (j->u.object.values[k].value->type == json_boolean) {
+										sqlite3_stmt *stmt = (sqlite3_stmt *)0;
+									  if (sqlite3_prepare_v2(_db,"UPDATE Member SET authorized = ? WHERE rowid = ?",-1,&stmt,(const char **)0) == SQLITE_OK)
+											sqlite3_bind_int(stmt,1,(j->u.object.values[k].value->u.boolean == 0) ? 0 : 1);
+											sqlite3_bind_int64(stmt,2,memberRowId);
+											sqlite3_step(stmt);
+											sqlite3_finalize(stmt);
+									}
+								} else if (!strcmp(j->u.object.values[k].name,"activeBridge")) {
+									if (j->u.object.values[k].value->type == json_boolean) {
+										sqlite3_stmt *stmt = (sqlite3_stmt *)0;
+									  if (sqlite3_prepare_v2(_db,"UPDATE Member SET activeBridge = ? WHERE rowid = ?",-1,&stmt,(const char **)0) == SQLITE_OK) {
+											sqlite3_bind_int(stmt,1,(j->u.object.values[k].value->u.boolean == 0) ? 0 : 1);
+											sqlite3_bind_int64(stmt,2,memberRowId);
+											sqlite3_step(stmt);
+											sqlite3_finalize(stmt);
+									  }
+									}
+								} else if (!strcmp(j->u.object.values[k].name,"ipAssignments")) {
+									if (j->u.object.values[k].value->type == json_array) {
+										sqlite3_reset(_sDeleteIpAllocations);
+										sqlite3_bind_text(_sDeleteIpAllocations,1,nwids,16,SQLITE_STATIC);
+										sqlite3_bind_text(_sDeleteIpAllocations,2,addrs,10,SQLITE_STATIC);
+										sqlite3_step(_sDeleteIpAllocations);
+										for(unsigned int kk=0;kk<j->u.object.values[k].value->u.array.length;++kk) {
+											json_value *ipalloc = j->u.object.values[k].value->u.array.values[kk];
+											if (ipalloc->type == json_string) {
+												InetAddress a(ipalloc->u.string.ptr);
+												char ipBlob[16];
+												int ipVersion = 0;
+												switch(a.ss_family) {
+													case AF_INET:
+														if ((a.netmaskBits() > 0)&&(a.netmaskBits() <= 32)) {
+															memset(ipBlob,0,12);
+															memcpy(ipBlob + 12,a.rawIpData(),4);
+															ipVersion = 4;
+														}
+														break;
+													case AF_INET6:
+														if ((a.netmaskBits() > 0)&&(a.netmaskBits() <= 128)) {
+															memcpy(ipBlob,a.rawIpData(),16);
+															ipVersion = 6;
+														}
+														break;
+												}
+												if (ipVersion > 0) {
+													sqlite3_reset(_sAllocateIp);
+													sqlite3_bind_text(_sAllocateIp,1,nwids,16,SQLITE_STATIC);
+													sqlite3_bind_text(_sAllocateIp,2,addrs,10,SQLITE_STATIC);
+													sqlite3_bind_blob(_sAllocateIp,3,(const void *)ipBlob,16,SQLITE_STATIC);
+													sqlite3_bind_int(_sAllocateIp,4,(int)a.netmaskBits());
+													sqlite3_bind_int(_sAllocateIp,5,ipVersion);
+													sqlite3_step(_sAllocateIp);
+												}
+											}
+										}
 									}
-									responseBody.append(",\n\t\"netconf\": \"");
-									responseBody.append(_jsonEscape(netconf.toString().c_str()));
-									responseBody.append("\",\n\t\"netconfResult\": \"");
-									responseBody.append(result);
-									responseBody.append("\",\n\t\"netconfResultMessage\": \"");
-									responseBody.append(_jsonEscape(netconf["error"].c_str()));
-									responseBody.append("\"");
-								} else {
-									responseBody.append(",\n\t\"netconf\": \"\",\n\t\"netconfResult\": \"INTERNAL_SERVER_ERROR\",\n\t\"netconfResultMessage\": \"invalid member or signing identity\"");
 								}
-							} catch ( ... ) {
-								responseBody.append(",\n\t\"netconf\": \"\",\n\t\"netconfResult\": \"INTERNAL_SERVER_ERROR\",\n\t\"netconfResultMessage\": \"unexpected exception\"");
+
 							}
 						}
+						json_value_free(j);
+					}
 
-						responseBody.append("\n}\n");
-
-						responseContentType = "application/json";
-						return 200;
-					} // else 404
+					return _doCPGet(path,urlArgs,headers,body,responseBody,responseContentType);
 				} // else 404
+
 			} else {
-				// get network info
-				sqlite3_reset(_sGetNetworkById);
-				sqlite3_bind_text(_sGetNetworkById,1,nwids,16,SQLITE_STATIC);
-				if (sqlite3_step(_sGetNetworkById) == SQLITE_ROW) {
-					Utils::snprintf(json,sizeof(json),
-						"{\n"
-						"\t\"nwid\": \"%s\",\n"
-						"\t\"name\": \"%s\",\n"
-						"\t\"private\": %s,\n"
-						"\t\"enableBroadcast\": %s,\n"
-						"\t\"allowPassiveBridging\": %s,\n"
-						"\t\"v4AssignMode\": \"%s\",\n"
-						"\t\"v6AssignMode\": \"%s\",\n"
-						"\t\"multicastLimit\": %d,\n"
-						"\t\"creationTime\": %llu,\n",
-						"\t\"revision\": %llu,\n"
-						"\a\"members\": [",
-						nwids,
-						_jsonEscape((const char *)sqlite3_column_text(_sGetNetworkById,0)).c_str(),
-						(sqlite3_column_int(_sGetNetworkById,1) > 0) ? "true" : "false",
-						(sqlite3_column_int(_sGetNetworkById,2) > 0) ? "true" : "false",
-						(sqlite3_column_int(_sGetNetworkById,3) > 0) ? "true" : "false",
-						_jsonEscape((const char *)sqlite3_column_text(_sGetNetworkById,4)).c_str(),
-						_jsonEscape((const char *)sqlite3_column_text(_sGetNetworkById,5)).c_str(),
-						sqlite3_column_int(_sGetNetworkById,6),
-						(unsigned long long)sqlite3_column_int64(_sGetNetworkById,7),
-						(unsigned long long)sqlite3_column_int64(_sGetNetworkById,8));
-					responseBody = json;
 
-					sqlite3_reset(_sListNetworkMembers);
-					sqlite3_bind_text(_sListNetworkMembers,1,nwids,16,SQLITE_STATIC);
-					bool firstMember = true;
-					while (sqlite3_step(_sListNetworkMembers) == SQLITE_ROW) {
-						if (!firstMember)
-							responseBody.push_back(',');
-						responseBody.push_back('"');
-						responseBody.append((const char *)sqlite3_column_text(_sListNetworkMembers,0));
-						responseBody.push_back('"');
-						firstMember = false;
-					}
-					responseBody.append("],\n\t\"relays\": [");
-
-					sqlite3_reset(_sGetRelays);
-					sqlite3_bind_text(_sGetRelays,1,nwids,16,SQLITE_STATIC);
-					bool firstRelay = true;
-					while (sqlite3_step(_sGetRelays) == SQLITE_ROW) {
-						responseBody.append(firstRelay ? "\n\t\t" : ",\n\t\t");
-						firstRelay = false;
-						responseBody.append("{\"address\":\"");
-						responseBody.append((const char *)sqlite3_column_text(_sGetRelays,0));
-						responseBody.append("\",\"phyAddress\":\"");
-						responseBody.append(_jsonEscape((const char *)sqlite3_column_text(_sGetRelays,1)));
-						responseBody.append("\"}");
-					}
-					responseBody.append("],\n\t\"ipAssignmentPools\": [");
-
-					sqlite3_reset(_sGetIpAssignmentPools2);
-					sqlite3_bind_text(_sGetIpAssignmentPools2,1,nwids,16,SQLITE_STATIC);
-					bool firstIpAssignmentPool = true;
-					while (sqlite3_step(_sGetIpAssignmentPools2) == SQLITE_ROW) {
-						responseBody.append(firstIpAssignmentPool ? "\n\t\t" : ",\n\t\t");
-						firstIpAssignmentPool = false;
-						InetAddress ipp((const void *)sqlite3_column_blob(_sGetIpAssignmentPools2,0),(sqlite3_column_int(_sGetIpAssignmentPools2,2) == 6) ? 16 : 4,(unsigned int)sqlite3_column_int(_sGetIpAssignmentPools2,1));
-						Utils::snprintf(json,sizeof(json),"{\"network\":\"%s\",\"netmaskBits\":%u}",
-							_jsonEscape(ipp.toIpString()).c_str(),
-							ipp.netmaskBits());
-						responseBody.append(json);
-					}
-					responseBody.append("],\n\t\"rules\": [");
-
-					sqlite3_reset(_sListRules);
-					sqlite3_bind_text(_sListRules,1,nwids,16,SQLITE_STATIC);
-					bool firstRule = true;
-					while (sqlite3_step(_sListRules) == SQLITE_ROW) {
-						responseBody.append(firstRule ? "\n\t{\n" : ",{\n");
-						Utils::snprintf(json,sizeof(json),"\t\t\"ruleId\": %lld,\n",sqlite3_column_int64(_sListRules,0));
-						responseBody.append(json);
-						if (sqlite3_column_type(_sListRules,1) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"nodeId\": \"%s\",\n",(const char *)sqlite3_column_text(_sListRules,1));
-							responseBody.append(json);
-						}
-						if (sqlite3_column_type(_sListRules,2) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"vlanId\": %d,\n",sqlite3_column_int(_sListRules,2));
-							responseBody.append(json);
-						}
-						if (sqlite3_column_type(_sListRules,3) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"vlanPcp\": %d,\n",sqlite3_column_int(_sListRules,3));
-							responseBody.append(json);
-						}
-						if (sqlite3_column_type(_sListRules,4) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"etherType\": %d,\n",sqlite3_column_int(_sListRules,4));
-							responseBody.append(json);
-						}
-						if (sqlite3_column_type(_sListRules,5) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"macSource\": \"%s\",\n",MAC((const char *)sqlite3_column_text(_sListRules,5)).toString().c_str());
-							responseBody.append(json);
-						}
-						if (sqlite3_column_type(_sListRules,6) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"macDest\": \"%s\",\n",MAC((const char *)sqlite3_column_text(_sListRules,6)).toString().c_str());
-							responseBody.append(json);
-						}
-						if (sqlite3_column_type(_sListRules,7) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"ipSource\": \"%s\",\n",_jsonEscape((const char *)sqlite3_column_text(_sListRules,7)).c_str());
-							responseBody.append(json);
-						}
-						if (sqlite3_column_type(_sListRules,8) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"ipDest\": \"%s\",\n",_jsonEscape((const char *)sqlite3_column_text(_sListRules,8)).c_str());
-							responseBody.append(json);
-						}
-						if (sqlite3_column_type(_sListRules,9) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"ipTos\": %d,\n",sqlite3_column_int(_sListRules,9));
-							responseBody.append(json);
-						}
-						if (sqlite3_column_type(_sListRules,10) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"ipProtocol\": %d,\n",sqlite3_column_int(_sListRules,10));
-							responseBody.append(json);
-						}
-						if (sqlite3_column_type(_sListRules,11) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"ipSourcePort\": %d,\n",sqlite3_column_int(_sListRules,11));
-							responseBody.append(json);
-						}
-						if (sqlite3_column_type(_sListRules,12) != SQLITE_NULL) {
-							Utils::snprintf(json,sizeof(json),"\t\t\"ipDestPort\": %d,\n",sqlite3_column_int(_sListRules,12));
-							responseBody.append(json);
-						}
-						responseBody.append("\t\t\"action\": \"");
-						responseBody.append(_jsonEscape((const char *)sqlite3_column_text(_sListRules,13)));
-						responseBody.append("\"\n\t}");
-					}
-
-					responseBody.append("]\n}\n");
-					responseContentType = "application/json";
-					return 200;
-				} // else 404
-			}
-		} else if (path.size() == 1) {
-			// list networks
-			sqlite3_reset(_sListNetworks);
-			responseContentType = "application/json";
-			responseBody = "[";
-			bool first = true;
-			while (sqlite3_step(_sListNetworks) == SQLITE_ROW) {
-				if (first) {
-					first = false;
-					responseBody.push_back('"');
-				} else responseBody.append(",\"");
-				responseBody.append((const char *)sqlite3_column_text(_sListNetworks,0));
-				responseBody.push_back('"');
-			}
-			responseBody.push_back(']');
-			return 200;
-		} // else 404
-
-	} else {
-		// GET /controller returns status and API version if controller is supported
-		Utils::snprintf(json,sizeof(json),"{\n\t\"controller\": true,\n\t\"apiVersion\": %d,\n\t\"clock\": %llu\n}",ZT_NETCONF_CONTROLLER_API_VERSION,(unsigned long long)OSUtils::now());
-		responseBody = json;
-		responseContentType = "applicaiton/json";
-		return 200;
-	}
-
-	return 404;
-}
-
-unsigned int SqliteNetworkController::handleControlPlaneHttpPOST(
-	const std::vector<std::string> &path,
-	const std::map<std::string,std::string> &urlArgs,
-	const std::map<std::string,std::string> &headers,
-	const std::string &body,
-	std::string &responseBody,
-	std::string &responseContentType)
-{
-	if (path.empty())
-		return 404;
-	Mutex::Lock _l(_lock);
-
-	if (path[0] == "network") {
-
-		if ((path.size() >= 2)&&(path[1].length() == 16)) {
-			uint64_t nwid = Utils::hexStrToU64(path[1].c_str());
-			char nwids[24];
-			Utils::snprintf(nwids,sizeof(nwids),"%.16llx",(unsigned long long)nwid);
-
-			int64_t revision = 0;
-			sqlite3_reset(_sGetNetworkRevision);
-			sqlite3_bind_text(_sGetNetworkRevision,1,nwids,16,SQLITE_STATIC);
-			bool networkExists = false;
-			if (sqlite3_step(_sGetNetworkRevision) == SQLITE_ROW) {
-				networkExists = true;
-				revision = sqlite3_column_int64(_sGetNetworkRevision,0);
-			}
-
-			if (path.size() >= 3) {
-
-				if (!networkExists)
-					return 404;
-
-				if ((path.size() == 4)&&(path[2] == "member")&&(path[3].length() == 10)) {
-					uint64_t address = Utils::hexStrToU64(path[3].c_str());
-					char addrs[24];
-					Utils::snprintf(addrs,sizeof(addrs),"%.10llx",address);
-
-					int64_t memberRowId = 0;
-					sqlite3_reset(_sGetMember);
-					sqlite3_bind_text(_sGetMember,1,nwids,16,SQLITE_STATIC);
-					sqlite3_bind_text(_sGetMember,2,addrs,10,SQLITE_STATIC);
-					bool memberExists = false;
-					if (sqlite3_step(_sGetMember) == SQLITE_ROW) {
-						memberExists = true;
-						memberRowId = sqlite3_column_int64(_sGetMember,0);
-					}
-
-					if (!memberExists) {
-						sqlite3_reset(_sCreateMember);
-						sqlite3_bind_text(_sCreateMember,1,nwids,16,SQLITE_STATIC);
-						sqlite3_bind_text(_sCreateMember,2,addrs,10,SQLITE_STATIC);
-						sqlite3_bind_int(_sCreateMember,3,0);
-						if (sqlite3_step(_sCreateMember) != SQLITE_DONE)
-							return 500;
-						memberRowId = (int64_t)sqlite3_last_insert_rowid(_db);
-					}
-
-					json_value *j = json_parse(body.c_str(),body.length());
-					if (j) {
-						if (j->type == json_object) {
-							for(unsigned int k=0;k<j->u.object.length;++k) {
-
-								if (!strcmp(j->u.object.values[k].name,"authorized")) {
-									if (j->u.object.values[k].value->type == json_boolean) {
-										sqlite3_stmt *stmt = (sqlite3_stmt *)0;
-									  if (sqlite3_prepare_v2(_db,"UPDATE Member SET authorized = ? WHERE rowid = ?",-1,&stmt,(const char **)0) == SQLITE_OK)
-											sqlite3_bind_int(stmt,1,(j->u.object.values[k].value->u.boolean == 0) ? 0 : 1);
-											sqlite3_bind_int64(stmt,2,memberRowId);
-											sqlite3_step(stmt);
-											sqlite3_finalize(stmt);
-									}
-								} else if (!strcmp(j->u.object.values[k].name,"activeBridge")) {
-									if (j->u.object.values[k].value->type == json_boolean) {
-										sqlite3_stmt *stmt = (sqlite3_stmt *)0;
-									  if (sqlite3_prepare_v2(_db,"UPDATE Member SET activeBridge = ? WHERE rowid = ?",-1,&stmt,(const char **)0) == SQLITE_OK) {
-											sqlite3_bind_int(stmt,1,(j->u.object.values[k].value->u.boolean == 0) ? 0 : 1);
-											sqlite3_bind_int64(stmt,2,memberRowId);
-											sqlite3_step(stmt);
-											sqlite3_finalize(stmt);
-									  }
-									}
-								} else if (!strcmp(j->u.object.values[k].name,"ipAssignments")) {
-									if (j->u.object.values[k].value->type == json_array) {
-										sqlite3_reset(_sDeleteIpAllocations);
-										sqlite3_bind_text(_sDeleteIpAllocations,1,nwids,16,SQLITE_STATIC);
-										sqlite3_bind_text(_sDeleteIpAllocations,2,addrs,10,SQLITE_STATIC);
-										sqlite3_step(_sDeleteIpAllocations);
-										for(unsigned int kk=0;kk<j->u.object.values[k].value->u.array.length;++kk) {
-											json_value *ipalloc = j->u.object.values[k].value->u.array.values[kk];
-											if (ipalloc->type == json_string) {
-												InetAddress a(ipalloc->u.string.ptr);
-												char ipBlob[16];
-												int ipVersion = 0;
-												switch(a.ss_family) {
-													case AF_INET:
-														if ((a.netmaskBits() > 0)&&(a.netmaskBits() <= 32)) {
-															memset(ipBlob,0,12);
-															memcpy(ipBlob + 12,a.rawIpData(),4);
-															ipVersion = 4;
-														}
-														break;
-													case AF_INET6:
-														if ((a.netmaskBits() > 0)&&(a.netmaskBits() <= 128)) {
-															memcpy(ipBlob,a.rawIpData(),16);
-															ipVersion = 6;
-														}
-														break;
-												}
-												if (ipVersion > 0) {
-													sqlite3_reset(_sAllocateIp);
-													sqlite3_bind_text(_sAllocateIp,1,nwids,16,SQLITE_STATIC);
-													sqlite3_bind_text(_sAllocateIp,2,addrs,10,SQLITE_STATIC);
-													sqlite3_bind_blob(_sAllocateIp,3,(const void *)ipBlob,16,SQLITE_STATIC);
-													sqlite3_bind_int(_sAllocateIp,4,(int)a.netmaskBits());
-													sqlite3_bind_int(_sAllocateIp,5,ipVersion);
-													sqlite3_step(_sAllocateIp);
-												}
-											}
-										}
-									}
-								}
-
-							}
-						}
-						json_value_free(j);
-					}
-
-					return handleControlPlaneHttpGET(path,urlArgs,headers,body,responseBody,responseContentType);
-				} // else 404
-
-			} else {
-
-				if (!networkExists) {
-					sqlite3_reset(_sCreateNetwork);
-					sqlite3_bind_text(_sCreateNetwork,1,nwids,16,SQLITE_STATIC);
-					sqlite3_bind_text(_sCreateNetwork,2,nwids,16,SQLITE_STATIC); // default name, will be changed below if a name is specified in JSON
-					sqlite3_bind_int64(_sCreateNetwork,3,(long long)OSUtils::now());
-					if (sqlite3_step(_sCreateNetwork) != SQLITE_DONE)
-						return 500;
-				}
+				if (!networkExists) {
+					sqlite3_reset(_sCreateNetwork);
+					sqlite3_bind_text(_sCreateNetwork,1,nwids,16,SQLITE_STATIC);
+					sqlite3_bind_text(_sCreateNetwork,2,nwids,16,SQLITE_STATIC); // default name, will be changed below if a name is specified in JSON
+					sqlite3_bind_int64(_sCreateNetwork,3,(long long)OSUtils::now());
+					if (sqlite3_step(_sCreateNetwork) != SQLITE_DONE)
+						return 500;
+				}
 
 				json_value *j = json_parse(body.c_str(),body.length());
 				if (j) {
@@ -1193,7 +922,7 @@ unsigned int SqliteNetworkController::handleControlPlaneHttpPOST(
 				sqlite3_bind_text(_sSetNetworkRevision,2,nwids,16,SQLITE_STATIC);
 				sqlite3_step(_sSetNetworkRevision);
 
-				return handleControlPlaneHttpGET(path,urlArgs,headers,body,responseBody,responseContentType);
+				return _doCPGet(path,urlArgs,headers,body,responseBody,responseContentType);
 			}
 
 		} // else 404
@@ -1258,4 +987,287 @@ unsigned int SqliteNetworkController::handleControlPlaneHttpDELETE(
 	return 404;
 }
 
+unsigned int SqliteNetworkController::_doCPGet(
+	const std::vector<std::string> &path,
+	const std::map<std::string,std::string> &urlArgs,
+	const std::map<std::string,std::string> &headers,
+	const std::string &body,
+	std::string &responseBody,
+	std::string &responseContentType)
+{
+	// Assumes _lock is locked
+	char json[16384];
+
+	if ((path.size() > 0)&&(path[0] == "network")) {
+
+		if ((path.size() >= 2)&&(path[1].length() == 16)) {
+			uint64_t nwid = Utils::hexStrToU64(path[1].c_str());
+			char nwids[24];
+			Utils::snprintf(nwids,sizeof(nwids),"%.16llx",(unsigned long long)nwid);
+
+			if (path.size() >= 3) {
+				if ((path.size() == 4)&&(path[2] == "member")&&(path[3].length() == 10)) {
+					uint64_t address = Utils::hexStrToU64(path[3].c_str());
+					char addrs[24];
+					Utils::snprintf(addrs,sizeof(addrs),"%.10llx",address);
+
+					sqlite3_reset(_sGetMember2);
+					sqlite3_bind_text(_sGetMember2,1,nwids,16,SQLITE_STATIC);
+					sqlite3_bind_text(_sGetMember2,2,addrs,10,SQLITE_STATIC);
+					if (sqlite3_step(_sGetMember2) == SQLITE_ROW) {
+						Utils::snprintf(json,sizeof(json),
+							"{\n"
+							"\t\"nwid\": \"%s\",\n"
+							"\t\"address\": \"%s\",\n"
+							"\t\"authorized\": %s,\n"
+							"\t\"activeBridge\": %s,\n"
+							"\t\"lastAt\": \"%s\",\n"
+							"\t\"lastSeen\": %llu,\n"
+							"\t\"firstSeen\": %llu,\n"
+							"\t\"identity\": \"%s\",\n"
+							"\t\"ipAssignments\": [",
+							nwids,
+							addrs,
+							(sqlite3_column_int(_sGetMember2,0) > 0) ? "true" : "false",
+							(sqlite3_column_int(_sGetMember2,1) > 0) ? "true" : "false",
+							_jsonEscape((const char *)sqlite3_column_text(_sGetMember2,3)).c_str(),
+							(unsigned long long)sqlite3_column_int64(_sGetMember2,4),
+							(unsigned long long)sqlite3_column_int64(_sGetMember2,5),
+							_jsonEscape((const char *)sqlite3_column_text(_sGetMember2,2)).c_str());
+						responseBody = json;
+
+						sqlite3_reset(_sGetIpAssignmentsForNode2);
+						sqlite3_bind_text(_sGetIpAssignmentsForNode2,1,nwids,16,SQLITE_STATIC);
+						sqlite3_bind_text(_sGetIpAssignmentsForNode2,2,addrs,10,SQLITE_STATIC);
+						bool firstIp = true;
+						while (sqlite3_step(_sGetIpAssignmentPools2) == SQLITE_ROW) {
+							InetAddress ip((const void *)sqlite3_column_blob(_sGetIpAssignmentsForNode2,0),(sqlite3_column_int(_sGetIpAssignmentsForNode2,2) == 6) ? 16 : 4,(unsigned int)sqlite3_column_int(_sGetIpAssignmentPools2,1));
+							responseBody.append(firstIp ? "\"" : ",\"");
+							firstIp = false;
+							responseBody.append(_jsonEscape(ip.toString()));
+							responseBody.push_back('"');
+						}
+
+						responseBody.append("]");
+
+						/* It's possible to get the actual netconf dictionary by including these
+						 * three URL arguments. The member identity must be the string
+						 * serialized identity of this member, and the signing identity must be
+						 * the full secret identity of this network controller. The have revision
+						 * is optional but would designate the revision our hypothetical client
+						 * already has.
+						 *
+						 * This is primarily for testing and is not used in production. It makes
+						 * it easy to test the entire network controller via its JSON API.
+						 *
+						 * If these arguments are included, three more object fields are returned:
+						 * 'netconf', 'netconfResult', and 'netconfResultMessage'. These are all
+						 * string fields and contain the actual netconf dictionary, the query
+						 * result code, and any verbose message e.g. an error description. */
+						std::map<std::string,std::string>::const_iterator memids(urlArgs.find("memberIdentity"));
+						std::map<std::string,std::string>::const_iterator sigids(urlArgs.find("signingIdentity"));
+						std::map<std::string,std::string>::const_iterator hrs(urlArgs.find("haveRevision"));
+						if ((memids != urlArgs.end())&&(sigids != urlArgs.end())) {
+							Dictionary netconf;
+							Identity memid,sigid;
+							try {
+								if (memid.fromString(memids->second)&&sigid.fromString(sigids->second)&&sigid.hasPrivate()) {
+									uint64_t hr = 0;
+									if (hrs != urlArgs.end())
+										hr = Utils::strToU64(hrs->second.c_str());
+									const char *result = "";
+									switch(this->doNetworkConfigRequest(InetAddress(),sigid,memid,nwid,Dictionary(),hr,netconf)) {
+										case NetworkController::NETCONF_QUERY_OK: result = "OK"; break;
+										case NetworkController::NETCONF_QUERY_OK_BUT_NOT_NEWER: result = "OK_BUT_NOT_NEWER"; break;
+										case NetworkController::NETCONF_QUERY_OBJECT_NOT_FOUND: result = "OBJECT_NOT_FOUND"; break;
+										case NetworkController::NETCONF_QUERY_ACCESS_DENIED: result = "ACCESS_DENIED"; break;
+										case NetworkController::NETCONF_QUERY_INTERNAL_SERVER_ERROR: result = "INTERNAL_SERVER_ERROR"; break;
+										default: result = "(unrecognized result code)"; break;
+									}
+									responseBody.append(",\n\t\"netconf\": \"");
+									responseBody.append(_jsonEscape(netconf.toString().c_str()));
+									responseBody.append("\",\n\t\"netconfResult\": \"");
+									responseBody.append(result);
+									responseBody.append("\",\n\t\"netconfResultMessage\": \"");
+									responseBody.append(_jsonEscape(netconf["error"].c_str()));
+									responseBody.append("\"");
+								} else {
+									responseBody.append(",\n\t\"netconf\": \"\",\n\t\"netconfResult\": \"INTERNAL_SERVER_ERROR\",\n\t\"netconfResultMessage\": \"invalid member or signing identity\"");
+								}
+							} catch ( ... ) {
+								responseBody.append(",\n\t\"netconf\": \"\",\n\t\"netconfResult\": \"INTERNAL_SERVER_ERROR\",\n\t\"netconfResultMessage\": \"unexpected exception\"");
+							}
+						}
+
+						responseBody.append("\n}\n");
+
+						responseContentType = "application/json";
+						return 200;
+					} // else 404
+				} // else 404
+			} else {
+				// get network info
+				sqlite3_reset(_sGetNetworkById);
+				sqlite3_bind_text(_sGetNetworkById,1,nwids,16,SQLITE_STATIC);
+				if (sqlite3_step(_sGetNetworkById) == SQLITE_ROW) {
+					Utils::snprintf(json,sizeof(json),
+						"{\n"
+						"\t\"nwid\": \"%s\",\n"
+						"\t\"name\": \"%s\",\n"
+						"\t\"private\": %s,\n"
+						"\t\"enableBroadcast\": %s,\n"
+						"\t\"allowPassiveBridging\": %s,\n"
+						"\t\"v4AssignMode\": \"%s\",\n"
+						"\t\"v6AssignMode\": \"%s\",\n"
+						"\t\"multicastLimit\": %d,\n"
+						"\t\"creationTime\": %llu,\n"
+						"\t\"revision\": %llu,\n"
+						"\t\"members\": [",
+						nwids,
+						_jsonEscape((const char *)sqlite3_column_text(_sGetNetworkById,0)).c_str(),
+						(sqlite3_column_int(_sGetNetworkById,1) > 0) ? "true" : "false",
+						(sqlite3_column_int(_sGetNetworkById,2) > 0) ? "true" : "false",
+						(sqlite3_column_int(_sGetNetworkById,3) > 0) ? "true" : "false",
+						_jsonEscape((const char *)sqlite3_column_text(_sGetNetworkById,4)).c_str(),
+						_jsonEscape((const char *)sqlite3_column_text(_sGetNetworkById,5)).c_str(),
+						sqlite3_column_int(_sGetNetworkById,6),
+						(unsigned long long)sqlite3_column_int64(_sGetNetworkById,7),
+						(unsigned long long)sqlite3_column_int64(_sGetNetworkById,8));
+					responseBody = json;
+
+					sqlite3_reset(_sListNetworkMembers);
+					sqlite3_bind_text(_sListNetworkMembers,1,nwids,16,SQLITE_STATIC);
+					bool firstMember = true;
+					while (sqlite3_step(_sListNetworkMembers) == SQLITE_ROW) {
+						if (!firstMember)
+							responseBody.push_back(',');
+						responseBody.push_back('"');
+						responseBody.append((const char *)sqlite3_column_text(_sListNetworkMembers,0));
+						responseBody.push_back('"');
+						firstMember = false;
+					}
+					responseBody.append("],\n\t\"relays\": [");
+
+					sqlite3_reset(_sGetRelays);
+					sqlite3_bind_text(_sGetRelays,1,nwids,16,SQLITE_STATIC);
+					bool firstRelay = true;
+					while (sqlite3_step(_sGetRelays) == SQLITE_ROW) {
+						responseBody.append(firstRelay ? "\n\t\t" : ",\n\t\t");
+						firstRelay = false;
+						responseBody.append("{\"address\":\"");
+						responseBody.append((const char *)sqlite3_column_text(_sGetRelays,0));
+						responseBody.append("\",\"phyAddress\":\"");
+						responseBody.append(_jsonEscape((const char *)sqlite3_column_text(_sGetRelays,1)));
+						responseBody.append("\"}");
+					}
+					responseBody.append("],\n\t\"ipAssignmentPools\": [");
+
+					sqlite3_reset(_sGetIpAssignmentPools2);
+					sqlite3_bind_text(_sGetIpAssignmentPools2,1,nwids,16,SQLITE_STATIC);
+					bool firstIpAssignmentPool = true;
+					while (sqlite3_step(_sGetIpAssignmentPools2) == SQLITE_ROW) {
+						responseBody.append(firstIpAssignmentPool ? "\n\t\t" : ",\n\t\t");
+						firstIpAssignmentPool = false;
+						InetAddress ipp((const void *)sqlite3_column_blob(_sGetIpAssignmentPools2,0),(sqlite3_column_int(_sGetIpAssignmentPools2,2) == 6) ? 16 : 4,(unsigned int)sqlite3_column_int(_sGetIpAssignmentPools2,1));
+						Utils::snprintf(json,sizeof(json),"{\"network\":\"%s\",\"netmaskBits\":%u}",
+							_jsonEscape(ipp.toIpString()).c_str(),
+							ipp.netmaskBits());
+						responseBody.append(json);
+					}
+					responseBody.append("],\n\t\"rules\": [");
+
+					sqlite3_reset(_sListRules);
+					sqlite3_bind_text(_sListRules,1,nwids,16,SQLITE_STATIC);
+					bool firstRule = true;
+					while (sqlite3_step(_sListRules) == SQLITE_ROW) {
+						responseBody.append(firstRule ? "\n\t{\n" : ",{\n");
+						Utils::snprintf(json,sizeof(json),"\t\t\"ruleId\": %lld,\n",sqlite3_column_int64(_sListRules,0));
+						responseBody.append(json);
+						if (sqlite3_column_type(_sListRules,1) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"nodeId\": \"%s\",\n",(const char *)sqlite3_column_text(_sListRules,1));
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,2) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"vlanId\": %d,\n",sqlite3_column_int(_sListRules,2));
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,3) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"vlanPcp\": %d,\n",sqlite3_column_int(_sListRules,3));
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,4) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"etherType\": %d,\n",sqlite3_column_int(_sListRules,4));
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,5) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"macSource\": \"%s\",\n",MAC((const char *)sqlite3_column_text(_sListRules,5)).toString().c_str());
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,6) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"macDest\": \"%s\",\n",MAC((const char *)sqlite3_column_text(_sListRules,6)).toString().c_str());
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,7) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"ipSource\": \"%s\",\n",_jsonEscape((const char *)sqlite3_column_text(_sListRules,7)).c_str());
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,8) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"ipDest\": \"%s\",\n",_jsonEscape((const char *)sqlite3_column_text(_sListRules,8)).c_str());
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,9) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"ipTos\": %d,\n",sqlite3_column_int(_sListRules,9));
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,10) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"ipProtocol\": %d,\n",sqlite3_column_int(_sListRules,10));
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,11) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"ipSourcePort\": %d,\n",sqlite3_column_int(_sListRules,11));
+							responseBody.append(json);
+						}
+						if (sqlite3_column_type(_sListRules,12) != SQLITE_NULL) {
+							Utils::snprintf(json,sizeof(json),"\t\t\"ipDestPort\": %d,\n",sqlite3_column_int(_sListRules,12));
+							responseBody.append(json);
+						}
+						responseBody.append("\t\t\"action\": \"");
+						responseBody.append(_jsonEscape((const char *)sqlite3_column_text(_sListRules,13)));
+						responseBody.append("\"\n\t}");
+					}
+
+					responseBody.append("]\n}\n");
+					responseContentType = "application/json";
+					return 200;
+				} // else 404
+			}
+		} else if (path.size() == 1) {
+			// list networks
+			sqlite3_reset(_sListNetworks);
+			responseContentType = "application/json";
+			responseBody = "[";
+			bool first = true;
+			while (sqlite3_step(_sListNetworks) == SQLITE_ROW) {
+				if (first) {
+					first = false;
+					responseBody.push_back('"');
+				} else responseBody.append(",\"");
+				responseBody.append((const char *)sqlite3_column_text(_sListNetworks,0));
+				responseBody.push_back('"');
+			}
+			responseBody.push_back(']');
+			return 200;
+		} // else 404
+
+	} else {
+		// GET /controller returns status and API version if controller is supported
+		Utils::snprintf(json,sizeof(json),"{\n\t\"controller\": true,\n\t\"apiVersion\": %d,\n\t\"clock\": %llu\n}",ZT_NETCONF_CONTROLLER_API_VERSION,(unsigned long long)OSUtils::now());
+		responseBody = json;
+		responseContentType = "applicaiton/json";
+		return 200;
+	}
+
+	return 404;
+}
+
 } // namespace ZeroTier

+ 8 - 0
controller/SqliteNetworkController.hpp

@@ -80,6 +80,14 @@ public:
 		std::string &responseContentType);
 
 private:
+	unsigned int _doCPGet(
+		const std::vector<std::string> &path,
+		const std::map<std::string,std::string> &urlArgs,
+		const std::map<std::string,std::string> &headers,
+		const std::string &body,
+		std::string &responseBody,
+		std::string &responseContentType);
+
 	std::string _dbPath;
 	sqlite3 *_db;
 

+ 70 - 4
nodejs-zt1-client/index.js

@@ -18,9 +18,9 @@ ZT1Client.prototype._jsonGet = function(getPath,callback)
 		}
 	},function(error,response,body) {
 		if (error)
-			return callback(error,{});
+			return callback(error,null);
 		if (response.statusCode !== 200)
-			return callback(new Error('server responded with '+response.statusCode),{});
+			return callback(new Error('server responded with error: '+response.statusCode),null);
 		return callback(null,(typeof body === 'string') ? JSON.parse(body) : null);
 	});
 };
@@ -58,14 +58,80 @@ ZT1Client.prototype.status = function(callback)
 	}.bind(this));
 };
 
-ZT1Client.prototype.networks = function(callback)
+ZT1Client.prototype.getNetworks = function(callback)
 {
 	this._jsonGet('network',callback);
 };
 
-ZT1Client.prototype.controllerNetworks = function(callback)
+ZT1Client.prototype.getPeers = function(callback)
+{
+	this._jsonGet('peer',callback);
+};
+
+ZT1Client.prototype.listControllerNetworks = function(callback)
 {
 	this._jsonGet('controller/network',callback);
 };
 
+ZT1Client.prototype.getControllerNetwork = function(nwid,callback)
+{
+	this._jsonGet('controller/network/' + nwid,callback);
+};
+
+ZT1Client.prototype.saveControllerNetwork = function(network,callback)
+{
+	if ((typeof network.nwid !== 'string')||(network.nwid.length !== 16))
+		return callback(new Error('Missing required field: nwid'),null);
+
+	// The ZT1 service is type variation intolerant, so recreate our submission with the correct types
+	var n = {
+		nwid: network.nwid
+	};
+	if (network.name)
+		n.name = network.name.toString();
+	if ('private' in network)
+		n.private = (network.private) ? true : false;
+	if ('enableBroadcast' in network)
+		n.enableBroadcast = (network.enableBroadcast) ? true : false;
+	if ('allowPassiveBridging' in network)
+		n.allowPassiveBridging = (network.allowPassiveBridging) ? true : false;
+	if ('v4AssignMode' in network) {
+		if (network.v4AssignMode)
+			n.v4AssignMode = network.v4AssignMode.toString();
+		else n.v4AssignMode = 'none';
+	}
+	if ('v6AssignMode' in network) {
+		if (network.v6AssignMode)
+			n.v6AssignMode = network.v6AssignMode.toString();
+		else n.v4AssignMode = 'none';
+	}
+	if ('multicastLimit' in network) {
+		if (typeof network.multicastLimit === 'number')
+			n.multicastLimit = network.multicastLimit;
+		else n.multicastLimit = parseInt(network.multicastLimit.toString());
+	}
+	if (Array.isArray(network.relays))
+		n.relays = network.relays;
+	if (Array.isArray(network.ipAssignmentPools))
+		n.ipAssignmentPools = network.ipAssignmentPools;
+	if (Array.isArray(network.rules))
+		n.rules = network.rules;
+
+	request({
+		url: this.url + 'controller/network/' + n.nwid,
+		method: 'POST',
+		json: true,
+		body: n,
+		headers: {
+			'X-ZT1-Auth': this.authToken
+		}
+	},function(err,response,body) {
+		if (err)
+			return callback(err,null);
+		if (response.statusCode !== 200)
+			return callback(new Error('server responded with error: '+response.statusCode),null);
+		return callback(null,(typeof body === 'string') ? JSON.parse(body) : body);
+	});
+};
+
 exports.ZT1Client = ZT1Client;

+ 0 - 14
nodejs-zt1-client/test-controller.js

@@ -1,14 +0,0 @@
-var ZT1Client = require('./index.js').ZT1Client;
-
-var zt1c = new ZT1Client('http://127.0.0.1:9993/','5d6181b71fae2684f9cc64ed');
-
-zt1c.status(function(err,status) {
-	if (err)
-		console.log(err);
-	console.log(status);
-	zt1c.networks(function(err,networks) {
-		if (err)
-			console.log(err);
-		console.log(networks);
-	});
-});

+ 33 - 0
nodejs-zt1-client/test.js

@@ -0,0 +1,33 @@
+var ZT1Client = require('./index.js').ZT1Client;
+
+var zt1c = new ZT1Client('http://127.0.0.1:9993/','5d6181b71fae2684f9cc64ed');
+
+zt1c.status(function(err,status) {
+	if (err)
+		console.log(err);
+	else console.log(status);
+
+	zt1c.getNetworks(function(err,networks) {
+		if (err)
+			console.log(err);
+		else console.log(networks);
+
+		zt1c.getPeers(function(err,peers) {
+			if (err)
+				console.log(err);
+			else console.log(peers);
+
+			if (status.controller) {
+				zt1c.saveControllerNetwork({
+					nwid: status.address + 'dead01',
+					name: 'test network',
+					private: true
+				},function(err,network) {
+					if (err)
+						console.log(err);
+					else console.log(network);
+				});
+			}
+		});
+	});
+});