diff --git a/CMakeLists.txt b/CMakeLists.txt index 888c7a9..f0f8695 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,18 @@ cmake_minimum_required (VERSION 3.0) project (zt) find_package (Threads) +SET(requiredlibs) + +if (CENTRAL_API) + FIND_PACKAGE(CURL) + IF(CURL_FOUND) + INCLUDE_DIRECTORIES(${CURL_INCLUDE_DIR}) + SET(requiredlibs ${requiredlibs} ${CURL_LIBRARIES} ) + ELSE(CURL_FOUND) + MESSAGE(FATAL_ERROR "Could not find the CURL library and development files.") + ENDIF(CURL_FOUND) +endif () + # ----------------------------------------------------------------------------- # | PLATFORM/FEATURE AND IDE DETECTION | # ----------------------------------------------------------------------------- @@ -441,6 +453,11 @@ set_target_properties (${STATIC_LIB_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${INTERMEDIATE_LIBRARY_OUTPUT_PATH}) set_target_properties (${STATIC_LIB_NAME} PROPERTIES COMPILE_FLAGS "${ZT_FLAGS}") target_link_libraries (${STATIC_LIB_NAME} ${CMAKE_THREAD_LIBS_INIT}) + +if (CENTRAL_API) + target_link_libraries (${STATIC_LIB_NAME} ${CURL_LIBRARIES}) +endif () + if (BUILDING_WIN) target_link_libraries ( ${STATIC_LIB_NAME} @@ -462,6 +479,10 @@ target_link_libraries ( ${shlwapi_LIBRARY_PATH} ${iphlpapi_LIBRARY_PATH} zt_pic lwip_pic zto_pic natpmp_pic miniupnpc_pic) +if (CENTRAL_API) + target_link_libraries (${DYNAMIC_LIB_NAME} ${CURL_LIBRARIES}) +endif () + set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) if (BUILDING_ANDROID) @@ -523,6 +544,10 @@ if (SHOULD_BUILD_TESTS) target_link_libraries(nonblockingclient ${STATIC_LIB_NAME}) add_executable (nonblockingserver ${PROJ_DIR}/examples/cpp/nonblockingserver.cpp) target_link_libraries(nonblockingserver ${STATIC_LIB_NAME}) +if (CENTRAL_API) + add_executable (centralapi ${PROJ_DIR}/examples/cpp/centralapi.cpp) + target_link_libraries(centralapi ${STATIC_LIB_NAME}) +endif () endif () # ----------------------------------------------------------------------------- @@ -543,13 +568,11 @@ install (TARGETS ${DYNAMIC_LIB_NAME} ) # ----------------------------------------------------------------------------- -# | TESTS | +# | CI TESTS | # ----------------------------------------------------------------------------- add_executable (errortest ${PROJ_DIR}/test/error.cpp) target_link_libraries(errortest ${STATIC_LIB_NAME}) - project (TEST) -#add_subdirectory (test) enable_testing () add_test (NAME MyTest COMMAND errortest) diff --git a/examples/cpp/centralapi.cpp b/examples/cpp/centralapi.cpp new file mode 100644 index 0000000..bd0e933 --- /dev/null +++ b/examples/cpp/centralapi.cpp @@ -0,0 +1,105 @@ +#include +#include +#include + +#include +#include +#include + +#include "ZeroTierSockets.h" + +// For optional JSON parsing +#include "../ext/ZeroTierOne/ext/json/json.hpp" + +void process_response(char *response, int http_response_code) +{ + if (http_response_code == 0) { + // Request failed at library level, do nothing. There would be no HTTP code at this point. + return; + } + printf("Raw response string (%d) = %s\n", http_response_code, response); + // Parse into navigable JSON object + if (http_response_code < 200 || http_response_code >= 300) { + return; + } + nlohmann::json res = nlohmann::json::parse(response); + if (!res.is_object()) { + fprintf(stderr, "Unable to parse (root element is not a JSON object)"); + } + // Pretty print JSON blob + std::cout << std::setw(4) << res << std::endl; +} + +int main(int argc, char **argv) +{ + if (argc != 3) { + printf("\nlibzt example central API client\n"); + printf("centralapi \n"); + exit(0); + } + std::string central_url = argv[1]; // API endpoint + std::string api_token = argv[2]; // User token (generate at my.zerotier.com) + + /** + * This example demonstrates how to use the ZeroTier Central API to: + * + * - Get the status of our hosted service (or your own) + * - Create a network + * - Get the full configuration of a network + * - Authorize/Deauthorize nodes on a network + * + * This example does not start a node (though you can if you wish.) This portion of the + * libzt API is merely a wrapper around our web API endpoint (https://my.zerotier.com/help/api). + * The HTTP requests are done via libcurl. This API is thread-safe but not multithreaded. + * + * Error Codes: + * -2 : [ZTS_ERR_SERVICE] The API may not have been initialized properly + * -3 : [ZTS_ERR_ARG] Invalid argument + * [100-500] : Standard HTTP error codes + * + * Usage example: centralapi https://my.zerotier.com e7no7nVRFItge7no7cVR5Ibge7no8nV1 + * + */ + + int err = ZTS_ERR_OK; + // Buffer to store server response as JSON string blobs + char rbuf[CENTRAL_API_RESP_BUF_DEFAULT_SZ]; + + // Provide URL to Central API server and user API token generated at https://my.zerotier.com + printf("Initializing Central API client...\n"); + if ((err = zts_central_api_init(central_url.c_str(), api_token.c_str(), rbuf, CENTRAL_API_RESP_BUF_DEFAULT_SZ)) != ZTS_ERR_OK) { + fprintf(stderr, "Error while initializing client's Central API parameters\n"); + return 0; + } + + zts_central_api_set_verbose(false); // (optiona) Turn on reporting from libcurl + zts_central_api_set_access(ZTS_CENTRAL_READ | ZTS_CENTRAL_WRITE); + + int http_res_code = 0; + + // Get hosted service status + printf("Requesting Central API server status (/api/status):\n"); + if ((err = zts_central_api_get_status(&http_res_code)) != ZTS_ERR_OK) { + fprintf(stderr, "Error (%d) making the request.\n", err); + } else { + process_response(rbuf, http_res_code); + } + // Get network config + int64_t nwid = 0x1234567890abcdef; + printf("Requesting network config: /api/network/%llx\n", nwid); + if ((err = zts_central_api_get_network(&http_res_code, nwid)) != ZTS_ERR_OK) { + fprintf(stderr, "Error (%d) making the request.\n", err); + } else { + process_response(rbuf, http_res_code); + } + // Authorize a node on a network + int64_t nodeid = 0x9934343434; + printf("Authorizing: /api/network/%llx/member/%llx\n", nwid, nodeid); + if ((err = zts_set_node_auth(&http_res_code, nwid, nodeid, ZTS_CENTRAL_NODE_AUTH_TRUE)) != ZTS_ERR_OK) { + fprintf(stderr, "Error (%d) making the request.\n", err); + } else { + process_response(rbuf, http_res_code); + } + + return 0; +} \ No newline at end of file diff --git a/src/Central.cpp b/src/Central.cpp new file mode 100644 index 0000000..35a5bcd --- /dev/null +++ b/src/Central.cpp @@ -0,0 +1,299 @@ +/* + * Copyright (c)2013-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. + */ +/****/ + +#ifndef ZT_CENTRAL_H +#define ZT_CENTRAL_H + +#ifdef CENTRAL_API + +#include +#include +#include +#include +#include + +#include "Mutex.hpp" +#include "Debug.hpp" +#include "ZeroTierSockets.h" + +char central_api_url[CENRTAL_API_MAX_URL_LEN]; +char central_api_token[CENTRAL_API_TOKEN_LEN+1]; + +char *_response_buffer; +int _response_buffer_len; +int _response_buffer_offset; + +static int8_t _api_access_modes; +static int8_t _bIsVerbose; +static int8_t _bInit; + +using namespace ZeroTier; + +Mutex _responseBuffer_m; + +#ifdef __cplusplus +extern "C" { +#endif + +size_t on_data(void *buffer, size_t size, size_t nmemb, void *userp) +{ + DEBUG_INFO("buffer=%p, size=%zu, nmemb=%zu, userp=%p", buffer, size, nmemb, userp); + int byte_count = (size * nmemb); + if (_response_buffer_offset + byte_count >= _response_buffer_len) { + DEBUG_ERROR("Out of buffer space. Cannot store response from server"); + return 0; // Signal to libcurl that our buffer is full (triggers a write error.) + } + memcpy(_response_buffer+_response_buffer_offset, buffer, byte_count); + _response_buffer_offset += byte_count; + return byte_count; +} + +void zts_central_api_set_access(int8_t modes) +{ + _api_access_modes = modes; +} + +void zts_central_api_set_verbose(int8_t is_verbose) +{ + _bIsVerbose = is_verbose; +} + +void zts_central_api_clear_response_buffer() +{ + Mutex::Lock _l(_responseBuffer_m); + memset(_response_buffer, 0, _response_buffer_len); + _response_buffer_offset = 0; +} + +int zts_central_api_init(const char *url_str, const char *token_str, char *response_buffer, uint32_t response_buffer_len) +{ + _api_access_modes = ZTS_CENTRAL_READ; // Defauly read-only + _bIsVerbose = 0; // Default disable libcurl verbose output + Mutex::Lock _l(_responseBuffer_m); + if (response_buffer_len == 0) { + return ZTS_ERR_ARG; + } + _response_buffer = response_buffer; + _response_buffer_len = response_buffer_len; + _response_buffer_offset = 0; + // Initialize all curl internal submodules + curl_global_init(CURL_GLOBAL_ALL); + + int url_len = strlen(url_str); + if (url_len < 3 || url_len > CENRTAL_API_MAX_URL_LEN) { + return ZTS_ERR_ARG; + } else { + memset(central_api_url, 0, CENRTAL_API_MAX_URL_LEN); + memcpy(central_api_url, url_str, url_len); + } + int token_len = strlen(token_str); + if (token_len != CENTRAL_API_TOKEN_LEN) { + return ZTS_ERR_ARG; + } else { + memset(central_api_token, 0, CENTRAL_API_TOKEN_LEN); + memcpy(central_api_token, token_str, token_len); + } + _bInit = true; + return ZTS_ERR_OK; +} + +void zts_central_api_cleanup() +{ + curl_global_cleanup(); +} + +int _central_req(int request_type, char *central_api_str, + char *api_route_str, char *token_str, int *response_code, char *post_data) +{ + int err = ZTS_ERR_OK; + if (!_bInit) { + DEBUG_ERROR("Error: Central API must be initialized first. Call zts_central_api_init()"); + return ZTS_ERR_SERVICE; + } + if (request_type == HTTP_GET && !(_api_access_modes & ZTS_CENTRAL_READ)) { + DEBUG_ERROR("Error: Incorrect access mode. Need (ZTS_CENTRAL_READ) permission"); + return ZTS_ERR_SERVICE; + } + if (request_type == HTTP_POST && !(_api_access_modes & ZTS_CENTRAL_WRITE)) { + DEBUG_ERROR("Error: Incorrect access mode. Need (ZTS_CENTRAL_WRITE) permission"); + return ZTS_ERR_SERVICE; + } + zts_central_api_clear_response_buffer(); + int central_api_strlen = strlen(central_api_str); + int api_route_strlen = strlen(api_route_str); + int token_strlen = strlen(token_str); + int url_len = central_api_strlen + api_route_strlen; + if (token_strlen > CENTRAL_API_TOKEN_LEN) { + return ZTS_ERR_ARG; + } + if (url_len > CENRTAL_API_MAX_URL_LEN) { + return ZTS_ERR_ARG; + } + char req_url[CENRTAL_API_MAX_URL_LEN]; + strcpy(req_url, central_api_str); + strcat(req_url, api_route_str); + + CURL *curl; + CURLcode res; + curl = curl_easy_init(); + if (!curl) { + return ZTS_ERR_GENERAL; + } + + struct curl_slist *hs=NULL; + char auth_str[CENTRAL_API_TOKEN_LEN + 32]; + if (token_strlen == CENTRAL_API_TOKEN_LEN) { + memset(auth_str, 0, CENTRAL_API_TOKEN_LEN + 32); + sprintf(auth_str, "Authorization: Bearer %s", token_str); + } + + hs = curl_slist_append(hs, auth_str); + hs = curl_slist_append(hs, "Content-Type: application/json"); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hs); + curl_easy_setopt(curl, CURLOPT_URL, req_url); + // example.com is redirected, so we tell libcurl to follow redirection + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + if (_bIsVerbose) { + curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); + } + // Tell curl to use our write function + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, on_data); + + if (request_type == HTTP_GET) { + // Nothing + DEBUG_INFO("Request (GET) = %s", api_route_str); + } + if (request_type == HTTP_POST) { + DEBUG_INFO("Request (POST) = %s", api_route_str); + if (post_data) { + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data); + } + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST"); + } + if (request_type == HTTP_DELETE) { + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); + } + //curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); // Consider 400-500 series code as failures + // Perform request + res = curl_easy_perform(curl); + if(res == CURLE_OK) { + //char* url; + double elapsed_time = 0.0; + long hrc = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &hrc); + curl_easy_getinfo(curl, CURLINFO_TOTAL_TIME, &elapsed_time); + DEBUG_INFO("Request took %f second(s). HTTP code (%ld)", elapsed_time, hrc); + *response_code = hrc; + //curl_easy_getinfo(curl, CURLINFO_EFFECTIVE_URL, &url); + } else { + DEBUG_ERROR("%s", curl_easy_strerror(res)); + err = ZTS_ERR_SERVICE; + } + curl_easy_cleanup(curl); + return err; +} + +int zts_get_last_response_buffer(char *dest_buffer, int dest_buffer_len) +{ + if (dest_buffer_len <= _response_buffer_offset) { + return ZTS_ERR_ARG; + } + int amount_to_copy = dest_buffer_len < _response_buffer_len ? dest_buffer_len : _response_buffer_len; + memcpy(dest_buffer, _response_buffer, amount_to_copy); + return ZTS_ERR_OK; +} + +int zts_central_api_get_status(int *http_response_code) +{ + return _central_req(HTTP_GET, central_api_url, (char*)"/api/status", central_api_token, http_response_code, NULL); +} + +int zts_central_api_get_self(int *http_response_code) +{ + return _central_req(HTTP_GET, central_api_url, (char*)"/api/self", central_api_token, http_response_code, NULL); +} + +int zts_central_api_get_network(int *http_response_code, int64_t nwid) +{ + char req[64]; + sprintf(req, "/api/network/%llx", nwid); + return _central_req(HTTP_GET, central_api_url, req, central_api_token, http_response_code, NULL); +} + +int zts_central_api_update_network(int *http_response_code, int64_t nwid) +{ + char req[64]; + sprintf(req, "/api/network/%llx", nwid); + return _central_req(HTTP_POST, central_api_url, req, central_api_token, http_response_code, NULL); +} + +int zts_central_api_delete_network(int *http_response_code, int64_t nwid) +{ + char req[64]; + sprintf(req, "/api/network/%llx", nwid); + return _central_req(HTTP_DELETE, central_api_url, req, central_api_token, http_response_code, NULL); +} + +int zts_central_api_get_networks(int *http_response_code) +{ + return _central_req(HTTP_GET, central_api_url, (char*)"/api/network", central_api_token, http_response_code, NULL); +} + +int zts_central_api_get_member(int *http_response_code, int64_t nwid, int64_t nodeid) +{ + if (nwid == 0 || nodeid == 0) { + return ZTS_ERR_ARG; + } + char req[64]; + sprintf(req, "/api/network/%llx/member/%llx", nwid, nodeid); + return _central_req(HTTP_GET, central_api_url, req, central_api_token, http_response_code, NULL); +} + +int zts_central_api_update_member(int *http_response_code, int64_t nwid, int64_t nodeid, char *post_data) +{ + if (nwid == 0 || nodeid == 0 || post_data == NULL) { + return ZTS_ERR_ARG; + } + char req[64]; + sprintf(req, "/api/network/%llx/member/%llx", nwid, nodeid); + return _central_req(HTTP_POST, central_api_url, req, central_api_token, http_response_code, post_data); +} + +int zts_set_node_auth(int *http_response_code, int64_t nwid, int64_t nodeid, int8_t is_authed) +{ + if (is_authed != 0 && is_authed != 1) { + return ZTS_ERR_ARG; + } + char config_data[64]; + if (is_authed == ZTS_CENTRAL_NODE_AUTH_TRUE) { + sprintf(config_data, "{\"config\": {\"authorized\": true} }"); + } + if (is_authed == ZTS_CENTRAL_NODE_AUTH_FALSE) { + sprintf(config_data, "{\"config\": {\"authorized\": false} }"); + } + return zts_central_api_update_member(http_response_code, nwid, nodeid, config_data); +} + +int zts_central_api_get_members_of_network(int *http_response_code, int64_t nwid) +{ + char req[64]; + sprintf(req, "/api/network/%llx/member", nwid); + return _central_req(HTTP_GET, central_api_url, req, central_api_token, http_response_code, NULL); +} + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // NO_CENTRAL_API +#endif // _H