Skip to content

Commit

Permalink
Switch to libcurl
Browse files Browse the repository at this point in the history
Now cloudflare-ddns is faster and much lighter.
It now checks and updates a DNS record with
less than 20 allocations and using roughly 20 kilobyes of RAM,
that is nothing compared to the 300+ allocations
of the previous version, that used cpr.
cloudflare-ddns now depends only on libcurl and libsimdjson,
and should be much easier to package.
  • Loading branch information
Tachi107 committed Feb 28, 2021
1 parent cd794ed commit b919d26
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 75 deletions.
31 changes: 31 additions & 0 deletions .github/workflows/cmake.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: CMake

on: [push]

jobs:
build:
runs-on: ubuntu-20.04

steps:
- uses: actions/checkout@v2

- name: Install dependencies
run: |
sudo rm /etc/apt/sources.list /etc/apt/sources.list.d/*
printf "Enabled: yes\nTypes: deb\nURIs: http://azure.archive.ubuntu.com/ubuntu/\nSuites: focal focal-updates focal-backports focal-security\nComponents: main universe\n" | sudo tee /etc/apt/sources.list.d/system.sources
sudo apt-get -qq update && sudo apt-get -qq install --assume-yes cmake libssl-dev
- name: Configure CMake
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release

- name: Build
run: cmake --build build

- name: Test
run: ctest -C build

- name: Store compiled program
uses: actions/upload-artifact@v2
with:
name: cloudflare-ddns-static-main
path: ${{runner.workspace}}/cloudflare-ddns/build/cloudflare-ddns
2 changes: 1 addition & 1 deletion .github/workflows/meson.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
sudo rm /etc/apt/sources.list /etc/apt/sources.list.d/*
printf "Enabled: yes\nTypes: deb\nURIs: http://azure.archive.ubuntu.com/ubuntu/\nSuites: focal focal-updates focal-backports focal-security devel\nComponents: main universe multiverse restricted\n" | sudo tee /etc/apt/sources.list.d/system.sources
printf "APT::Default-Release \"focal\";\n" | sudo tee /etc/apt/apt.conf.d/00default-release
sudo apt-get -qq update && sudo apt-get -qq install --assume-yes meson/hirsute libsimdjson-dev/hirsute nlohmann-json3-dev libcurl4-openssl-dev
sudo apt-get -qq update && sudo apt-get -qq install --assume-yes meson libsimdjson-dev libcurl4-openssl-dev
- name: Configure Meson
run: meson setup build --buildtype=release
Expand Down
30 changes: 12 additions & 18 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
cmake_minimum_required(VERSION 3.15)
cmake_minimum_required(VERSION 3.14)

project(cloudflare-ddns)

if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-fuse-ld=lld -Wno-unused-command-line-argument)
add_link_options(-fuse-ld=lld)
endif()

include(CheckIPOSupported)
check_ipo_supported(RESULT ipo_result OUTPUT ipo_output)
if(ipo_result)
Expand All @@ -15,20 +10,19 @@ if(ipo_result)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON)
endif()

add_compile_definitions(OPENSSL_NO_SSL3_METHOD)

include(FetchContent)

FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/whoshuu/cpr.git GIT_TAG 1.5.2)
FetchContent_Declare(curl GIT_REPOSITORY https://github.com/curl/curl.git GIT_TAG curl-7_75_0)
set(BUILD_CURL_EXE OFF CACHE BOOL "")
set(BUILD_SHARED_LIBS OFF CACHE BOOL "")
set(BUILD_CPR_TESTS OFF CACHE BOOL "")
add_compile_definitions(OPENSSL_NO_SSL3_METHOD)
FetchContent_MakeAvailable(cpr)

FetchContent_Declare(json GIT_REPOSITORY https://github.com/ArthurSonzogni/nlohmann_json_cmake_fetchcontent.git GIT_TAG v3.9.1)
FetchContent_GetProperties(json)
if(NOT json_POPULATED)
FetchContent_Populate(json)
add_subdirectory(${json_SOURCE_DIR} ${json_BINARY_DIR} EXCLUDE_FROM_ALL)
endif()
set(CURL_LTO ON CACHE BOOL "")
set(HTTP_ONLY ON CACHE BOOL "")
set(CURL_DISABLE_COOKIES ON CACHE BOOL "")
set(CURL_DISABLE_VERBOSE_STRINGS ON CACHE BOOL "")
set(ENABLE_UNIX_SOCKETS OFF CACHE BOOL "")
FetchContent_MakeAvailable(curl)

FetchContent_Declare(simdjson GIT_REPOSITORY https://github.com/simdjson/simdjson.git GIT_TAG v0.8.2 GIT_SHALLOW TRUE)
set(SIMDJSON_JUST_LIBRARY ON CACHE INTERNAL "")
Expand All @@ -37,4 +31,4 @@ FetchContent_MakeAvailable(simdjson)

add_executable(${PROJECT_NAME} cloudflare-ddns/main.cpp)

target_link_libraries(${PROJECT_NAME} PRIVATE cpr::cpr nlohmann_json::nlohmann_json simdjson)
target_link_libraries(${PROJECT_NAME} PRIVATE simdjson libcurl)
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
# cloudflare-ddns

cloudflare-ddns is a tool that can be used to dynamically update a DNS record using Cloudflare's API.
cloudflare-ddns is a tool that can be used to dynamically update a DNS record using Cloudflare's API.

## Usage

This tool is a oneshot program: you run it, it updates the DNS record, and it terminates. To make it run periodically you could use it with a systemd timer or a cron job.
This tool is a oneshot program: you run it, it updates the DNS record, and it terminates. To make it run periodically you could use a systemd timer or a cron job.

To run the tool you'll need an [API token](https://dash.cloudflare.com/profile/api-tokens) and the Zone ID of the DNS that you want to update; you can get the latter in the Overview panel of your zone.

## Dependencies

cloudflare-ddns currently relies on fairly new versions of libcurl and simdjson. To be able to run the utility you need to install `libcurl4` and `libsimdjson5` on your system.
cloudflare-ddns relies only on libcurl and simdjson. I recommend you to compile the program yourself, but I also provide a statically linked executable for every release. It is not 100% self-contained but it should work in most cases.

## Build

To build cloudflare-ddns you need to install `meson`, `cmake`, `libcurl4-openssl-dev`, `nlohmann-json3-dev` and a recent version of `libsimdjson-dev`. You should be fine with the packages available in Debian 11 and Ubuntu Hirsute.
To build cloudflare-ddns you'll need to install `meson`, `pkg-config`, `cmake`, `libcurl4-openssl-dev` and `libsimdjson-dev`. You should be fine with the packages available in Debian 10 (+backports) or Ubuntu Focal.

After having installed the dependencies, you can build the program with `meson setup build --buildtype=relese && meson compile -C build`
After having installed the dependencies, you can build the program with `meson setup build --buildtype=relese` and then `meson compile -C build`. If your Meson version is too old, you have to run `ninja -C build` instead of `meson compile`.

## Systemd timer

Expand Down
78 changes: 40 additions & 38 deletions cloudflare-ddns/main.cpp
Original file line number Diff line number Diff line change
@@ -1,59 +1,61 @@
#include <cpr/cpr.h>
#include <curl/curl.h>
#include <string>
#include <iostream>
#include <simdjson.h>
#include <nlohmann/json.hpp>

void libcurlSucks() {
curl_global_init(CURL_GLOBAL_SSL);

CURL* curlHandle {curl_easy_init()};
curl_easy_setopt(curlHandle, CURLOPT_NOPROGRESS, 1L);
curl_easy_setopt(curlHandle, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(curlHandle, CURLOPT_HTTPGET, 1L);

curl_global_cleanup();
std::size_t writeData(char* incomingBuffer, std::size_t size, std::size_t count, std::string* data) {
data->append(incomingBuffer, size * count);
return size * count;
}

int main(int argc, char* argv[]) {
if (argc != 4) {
std::cerr << "Usage: cloudflare-ddns <API token> <Zone ID> <DNS record name>\n";
return EXIT_FAILURE;
}
const std::string apiToken {argv[1]};
const std::string_view apiToken {argv[1]};
const std::string zoneId {argv[2]};
const std::string recordName {argv[3]};

CURL* curlHandle {curl_easy_init()};
std::string response;
response.reserve(600); // Tipical size of largest response (GET to fetch the DNS IP)
curl_easy_setopt(curlHandle, CURLOPT_NOPROGRESS, 1L);
curl_easy_setopt(curlHandle, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(curlHandle, CURLOPT_WRITEFUNCTION, writeData);
curl_easy_setopt(curlHandle, CURLOPT_WRITEDATA, &response);
curl_easy_setopt(curlHandle, CURLOPT_HTTPGET, 1L);
curl_easy_setopt(curlHandle, CURLOPT_URL, "https://1.1.1.1/cdn-cgi/trace");

simdjson::dom::parser parser;
const simdjson::dom::element parsed {
parser.parse(
cpr::Get(
cpr::Url{"https://api.cloudflare.com/client/v4/zones/" + zoneId + "/dns_records?type=A&name=" + recordName},
cpr::Header{
{"Content-Type", "application/json"},
{"Authorization", "Bearer " + apiToken}
}
// cpr::Bearer{apiToken} in cpr 1.6
).text
)
};
curl_easy_perform(curlHandle);
const std::size_t ipBegin {response.find("ip=") + 3}; // + 3 because "ip=" is 3 chars
const std::size_t ipEnd {response.find('\n', ipBegin)};
const std::string localIp {response.substr(ipBegin, ipEnd - ipBegin)};
response.clear();

curl_easy_setopt(curlHandle, CURLOPT_URL, std::string{"https://api.cloudflare.com/client/v4/zones/" + zoneId + "/dns_records?type=A&name=" + recordName}.data());
curl_easy_setopt(curlHandle, CURLOPT_HTTPAUTH, CURLAUTH_BEARER);
curl_easy_setopt(curlHandle, CURLOPT_XOAUTH2_BEARER, apiToken.data());
struct curl_slist* headers {nullptr};
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curlHandle, CURLOPT_HTTPHEADER, headers);

const std::string trace {cpr::Get(cpr::Url{"https://1.1.1.1/cdn-cgi/trace"}).text};
const std::size_t ipBegin {trace.find("ip=") + 3}; // + 3 because "ip=" is 3 chars
const std::size_t ipEnd {trace.find('\n', ipBegin)};
const std::string_view currentIp {std::string_view{trace}.substr(ipBegin, ipEnd - ipBegin)}; // Begin, length
curl_easy_perform(curlHandle);
simdjson::dom::parser parser;
const simdjson::dom::element parsed {parser.parse(response)};

if (currentIp != static_cast<std::string_view>((*parsed["result"].begin())["content"])) {
std::cout << "New IP: " << (parser.parse(cpr::Patch(
cpr::Url{"https://api.cloudflare.com/client/v4/zones/" + zoneId + "/dns_records/" + std::string{static_cast<std::string_view>((*parsed["result"].begin())["id"])}},
cpr::Header{
{"Content-Type", "application/json"},
{"Authorization", "Bearer " + apiToken}
},
cpr::Body{nlohmann::json{{"content", currentIp}}.dump()}
).text))["result"]["content"] << '\n';
if (localIp != static_cast<std::string_view>((*parsed["result"].begin())["content"])) {
response.clear();
curl_easy_setopt(curlHandle, CURLOPT_URL, std::string{"https://api.cloudflare.com/client/v4/zones/" + zoneId + "/dns_records/" + std::string{static_cast<std::string_view>((*parsed["result"].begin())["id"])}}.c_str());
// curl_easy_setopt(curlHandle, CURLOPT_NOBODY, 0L);
curl_easy_setopt(curlHandle, CURLOPT_CUSTOMREQUEST, "PATCH");
std::string request {R"({"content": ")" + localIp + "\"}"};
curl_easy_setopt(curlHandle, CURLOPT_POSTFIELDS, request.c_str());
curl_easy_perform(curlHandle);
std::cout << "New IP: " << parser.parse(response)["result"]["content"] << '\n';
}
else {
std::cout << "The DNS is up to date\n";
}
curl_easy_cleanup(curlHandle);
}
4 changes: 1 addition & 3 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,15 @@ project(
'b_ndebug=if-release',
'b_lto=true'
],
version: '0.0.1',
version: '0.1.0',
license: 'AGPL-3.0-or-later'
)

executable(
'cloudflare-ddns',
sources: 'cloudflare-ddns/main.cpp',
dependencies: [
dependency('cpr', fallback: ['cpr', 'cpr_dep']),
dependency('libcurl'),
dependency('nlohmann_json'),
dependency('simdjson')
],
cpp_args: '-DOPENSSL_NO_SSL3_METHOD'
Expand Down
10 changes: 0 additions & 10 deletions subprojects/cpr.wrap

This file was deleted.

0 comments on commit b919d26

Please sign in to comment.