diff --git a/src/main/java/codes/thischwa/cf/CfBasicHttpClient.java b/src/main/java/codes/thischwa/cf/CfBasicHttpClient.java index a2ed0c3..875f7fa 100644 --- a/src/main/java/codes/thischwa/cf/CfBasicHttpClient.java +++ b/src/main/java/codes/thischwa/cf/CfBasicHttpClient.java @@ -1,5 +1,6 @@ package codes.thischwa.cf; +import codes.thischwa.cf.auth.CfAuth; import codes.thischwa.cf.model.AbstractResponse; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -19,6 +20,7 @@ import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.message.BasicClassicHttpRequest; +import org.jetbrains.annotations.NotNull; /** * Abstract base class for creating HTTP clients to interact with the Cloudflare API. Provides @@ -27,23 +29,20 @@ import org.apache.hc.core5.http.message.BasicClassicHttpRequest; */ @Slf4j abstract class CfBasicHttpClient { - private final String baseUrl; - private final String authEmail; - private final String authKey; + private final String baseUrl; + private final CfAuth auth; private final ObjectMapper objectMapper; - CfBasicHttpClient(String baseUrl, String authEmail, String authKey) - throws IllegalArgumentException { - if (authEmail == null || authEmail.isBlank()) { - throw new IllegalArgumentException("Authentication email must not be null or blank!"); - } - if (authKey == null || authKey.isBlank()) { - throw new IllegalArgumentException("Authentication key must not be null or blank!"); - } + /** + * Creates a new Cloudflare HTTP client with the specified base URL and authentication. + * + * @param baseUrl the base URL for the Cloudflare API + * @param auth the authentication mechanism to use + */ + CfBasicHttpClient(@NotNull String baseUrl, @NotNull CfAuth auth) { this.baseUrl = baseUrl; - this.authEmail = authEmail; - this.authKey = authKey; + this.auth = auth; this.objectMapper = JsonConf.initObjectMapper(); } @@ -53,8 +52,9 @@ abstract class CfBasicHttpClient { request.addHeader(HttpHeaders.ACCEPT_ENCODING, "gzip"); request.addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.getMimeType()); request.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType()); - request.addHeader("X-Auth-Email", authEmail); - request.addHeader("X-Auth-Key", authKey); + if (request instanceof ClassicHttpRequest classicRequest) { + auth.applyAuth(classicRequest); + } }).build(); } diff --git a/src/main/java/codes/thischwa/cf/CfDnsClient.java b/src/main/java/codes/thischwa/cf/CfDnsClient.java index 3e78488..bdc6d5f 100644 --- a/src/main/java/codes/thischwa/cf/CfDnsClient.java +++ b/src/main/java/codes/thischwa/cf/CfDnsClient.java @@ -1,5 +1,6 @@ package codes.thischwa.cf; +import codes.thischwa.cf.auth.CfAuth; import codes.thischwa.cf.fluent.ZoneOperations; import codes.thischwa.cf.fluent.ZoneOperationsImpl; import codes.thischwa.cf.model.AbstractResponse; @@ -26,57 +27,71 @@ import org.jetbrains.annotations.Nullable; * records and zones within the Cloudflare system, including creating, updating, retrieving, and * deleting DNS records. * - *
Example: + *
Example with API token authentication (recommended): *
- * // Create a new CfDnsClient instance
- * CfDnsClient cfDnsClient = new CfDnsClient(
- * "email@example.com",
- * "yourApiKey"
- * );
+ * // Create a new CfDnsClient instance with API token
+ * CfDnsClient cfDnsClient = new CfDnsClient(CfAuthBuilder.build("your-api-token"));
+ *
* // Retrieve a zone
* ZoneEntity zone = cfDnsClient.zoneGet("example.com");
* System.out.println("Zone ID: " + zone.getId());
+ *
* // Retrieve records of a subdomain
- * List<{@link RecordEntity}> records = cfDnsClient.recordGet(zone, "sld");
+ * List<RecordEntity> records = cfDnsClient.recordList(zone, "sld");
* records.forEach(record ->
* System.out.println("Record Type: " + record.getType() + ", Value: " + record.getContent())
* );
+ *
* // Create a record for the subdomain "api"
* RecordEntity created = cfDnsClient.recordCreateSld(zone, "api", 60, RecordType.A, "192.168.1.10");
* System.out.println("Created Record ID: " + created.getId());
*
+ *
+ * Example with email/key authentication (legacy): + *
+ * CfDnsClient cfDnsClient = new CfDnsClient(
+ * CfAuthBuilder.build("email@example.com", "your-api-key")
+ * );
+ *
+ *
+ * Example with exception throwing enabled: + *
+ * // Throws exception when results are empty
+ * CfDnsClient cfDnsClient = new CfDnsClient(true, CfAuthBuilder.build("your-api-token"));
+ *
+ *
+ * Example with custom base URL: + *
+ * CfAuth auth = CfAuthBuilder.build("your-api-token");
+ * auth.setBaseUrl("https://custom-api.example.com");
+ * CfDnsClient cfDnsClient = new CfDnsClient(auth);
+ *
*/
@Slf4j
public class CfDnsClient extends CfBasicHttpClient {
- private static final String DEFAULT_BASEURL = "https://api.cloudflare.com/client/v4";
+ public static final String DEFAULT_BASEURL = "https://api.cloudflare.com/client/v4";
private final ResponseValidator responseValidator;
private final boolean emptyResultThrowsException;
/**
- * Constructs a new instance of {@code CfDnsClient}.
+ * Constructs a new instance of {@code CfDnsClient} with default configuration.
*
- * @param authEmail The email address associated with the Cloudflare account, used for
- * authentication.
- * @param authKey The API key of the Cloudflare account, used as part of the authentication
- * process.
+ * @param auth The authentication mechanism to use (ApiTokenAuth or EmailKeyAuth)
*/
- public CfDnsClient(String authEmail, String authKey) {
- this(DEFAULT_BASEURL, authEmail, authKey);
+ public CfDnsClient(CfAuth auth) {
+ this(false, DEFAULT_BASEURL, auth);
}
/**
* Constructs a new instance of {@code CfDnsClient}.
*
- * @param baseUrl The base URL of the Cloudflare API to be used for requests.
- * @param authEmail The email address associated with the Cloudflare account, used for
- * authentication.
- * @param authKey The API key of the Cloudflare account, used as part of the authentication
- * process.
+ * @param baseUrl The base URL of the Cloudflare API to be used for requests.
+ * @param auth The authentication mechanism to use (ApiTokenAuth or EmailKeyAuth)
*/
- public CfDnsClient(String baseUrl, String authEmail, String authKey) {
- this(false, baseUrl, authEmail, authKey);
+ public CfDnsClient(String baseUrl, CfAuth auth) {
+ this(false, baseUrl, auth);
}
/**
@@ -85,13 +100,10 @@ public class CfDnsClient extends CfBasicHttpClient {
* @param emptyResultThrowsException A boolean value indicating whether an exception should be
* thrown when the result is empty. Applies to both single and
* multiple result requests. Default is false.
- * @param authEmail The email address associated with the Cloudflare account,
- * used for authentication.
- * @param authKey The API key of the Cloudflare account, used as part of the
- * authentication process.
+ * @param auth The authentication mechanism to use (ApiTokenAuth or EmailKeyAuth)
*/
- public CfDnsClient(boolean emptyResultThrowsException, String authEmail, String authKey) {
- this(emptyResultThrowsException, DEFAULT_BASEURL, authEmail, authKey);
+ public CfDnsClient(boolean emptyResultThrowsException, CfAuth auth) {
+ this(emptyResultThrowsException, DEFAULT_BASEURL, auth);
}
/**
@@ -101,14 +113,10 @@ public class CfDnsClient extends CfBasicHttpClient {
* thrown when the result is empty. Applies to both single and
* multiple result requests. Default is false.
* @param baseUrl The base URL for the Cloudflare API endpoint.
- * @param authEmail The email associated with the Cloudflare account for
- * authentication.
- * @param authKey The API key for authenticating the client with Cloudflare
- * services.
+ * @param auth The authentication mechanism to use (ApiTokenAuth or EmailKeyAuth)
*/
- public CfDnsClient(boolean emptyResultThrowsException, String baseUrl, String authEmail,
- String authKey) {
- super(baseUrl, authEmail, authKey);
+ public CfDnsClient(boolean emptyResultThrowsException, String baseUrl, CfAuth auth) {
+ super(baseUrl, auth);
this.responseValidator = new ResponseValidator(emptyResultThrowsException);
this.emptyResultThrowsException = emptyResultThrowsException;
}
diff --git a/src/main/java/codes/thischwa/cf/auth/ApiTokenAuth.java b/src/main/java/codes/thischwa/cf/auth/ApiTokenAuth.java
new file mode 100644
index 0000000..8450df9
--- /dev/null
+++ b/src/main/java/codes/thischwa/cf/auth/ApiTokenAuth.java
@@ -0,0 +1,31 @@
+package codes.thischwa.cf.auth;
+
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Authentication mechanism using Cloudflare API token.
+ * This is the recommended authentication method for the Cloudflare API.
+ */
+public class ApiTokenAuth implements CfAuth {
+
+ private final String apiToken;
+
+ /**
+ * Creates a new API token authentication object.
+ *
+ * @param apiToken the Cloudflare API token
+ * @throws IllegalArgumentException if the API token is null or blank
+ */
+ public ApiTokenAuth(@NotNull String apiToken) {
+ if (apiToken.isBlank()) {
+ throw new IllegalArgumentException("API token must not be null or blank!");
+ }
+ this.apiToken = apiToken;
+ }
+
+ @Override
+ public void applyAuth(ClassicHttpRequest request) {
+ request.addHeader("Authorization", "Bearer " + apiToken);
+ }
+}
diff --git a/src/main/java/codes/thischwa/cf/auth/CfAuth.java b/src/main/java/codes/thischwa/cf/auth/CfAuth.java
new file mode 100644
index 0000000..5df4ad6
--- /dev/null
+++ b/src/main/java/codes/thischwa/cf/auth/CfAuth.java
@@ -0,0 +1,19 @@
+package codes.thischwa.cf.auth;
+
+import org.apache.hc.core5.http.ClassicHttpRequest;
+
+/**
+ * Interface for Cloudflare authentication mechanisms.
+ * Implementations of this interface provide different methods of authentication
+ * with the Cloudflare API (e.g., API token, email/key combination).
+ */
+public interface CfAuth {
+
+ /**
+ * Applies authentication headers to the given HTTP request.
+ *
+ * @param request the HTTP request to authenticate
+ */
+ void applyAuth(ClassicHttpRequest request);
+
+}
diff --git a/src/main/java/codes/thischwa/cf/auth/CfAuthBuilder.java b/src/main/java/codes/thischwa/cf/auth/CfAuthBuilder.java
new file mode 100644
index 0000000..05b3d46
--- /dev/null
+++ b/src/main/java/codes/thischwa/cf/auth/CfAuthBuilder.java
@@ -0,0 +1,12 @@
+package codes.thischwa.cf.auth;
+
+public class CfAuthBuilder {
+
+ public static ApiTokenAuth build(String apiToken) {
+ return new ApiTokenAuth(apiToken);
+ }
+
+ public static EmailKeyAuth build(String authEmail, String authKey) {
+ return new EmailKeyAuth(authEmail, authKey);
+ }
+}
diff --git a/src/main/java/codes/thischwa/cf/auth/EmailKeyAuth.java b/src/main/java/codes/thischwa/cf/auth/EmailKeyAuth.java
new file mode 100644
index 0000000..ab3e6ab
--- /dev/null
+++ b/src/main/java/codes/thischwa/cf/auth/EmailKeyAuth.java
@@ -0,0 +1,38 @@
+package codes.thischwa.cf.auth;
+
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Authentication mechanism using Cloudflare account email and API key.
+ * This is the legacy authentication method for the Cloudflare API.
+ */
+public class EmailKeyAuth implements CfAuth {
+
+ private final String authEmail;
+ private final String authKey;
+
+ /**
+ * Creates a new email/key authentication object.
+ *
+ * @param authEmail the email address associated with the Cloudflare account
+ * @param authKey the API key of the Cloudflare account
+ * @throws IllegalArgumentException if email or key is null or blank
+ */
+ public EmailKeyAuth(@NotNull String authEmail, @NotNull String authKey) {
+ if (authEmail.isBlank()) {
+ throw new IllegalArgumentException("Authentication email must not be null or blank!");
+ }
+ if (authKey.isBlank()) {
+ throw new IllegalArgumentException("Authentication key must not be null or blank!");
+ }
+ this.authEmail = authEmail;
+ this.authKey = authKey;
+ }
+
+ @Override
+ public void applyAuth(ClassicHttpRequest request) {
+ request.addHeader("X-Auth-Email", authEmail);
+ request.addHeader("X-Auth-Key", authKey);
+ }
+}
diff --git a/src/main/java/codes/thischwa/cf/auth/package-info.java b/src/main/java/codes/thischwa/cf/auth/package-info.java
new file mode 100644
index 0000000..c57458f
--- /dev/null
+++ b/src/main/java/codes/thischwa/cf/auth/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * The authentication package of CloudflareDNS-java.
+ */
+
+package codes.thischwa.cf.auth;
diff --git a/src/main/java/codes/thischwa/cf/model/RecordEntity.java b/src/main/java/codes/thischwa/cf/model/RecordEntity.java
index b982e29..7fb32d3 100644
--- a/src/main/java/codes/thischwa/cf/model/RecordEntity.java
+++ b/src/main/java/codes/thischwa/cf/model/RecordEntity.java
@@ -64,10 +64,10 @@ public class RecordEntity extends AbstractEntity {
*/
public static RecordEntity build(String name, RecordType type, Integer ttl, String content) {
RecordEntity rec = new RecordEntity();
- rec.setName(name);
- rec.setType(type.getType());
- rec.setTtl(ttl);
- rec.setContent(content);
+ rec.name = name;
+ rec.type = type.getType();
+ rec.ttl = ttl;
+ rec.content = content;
return rec;
}
@@ -81,7 +81,7 @@ public class RecordEntity extends AbstractEntity {
public static RecordEntity build(String id, String content) {
RecordEntity rec = new RecordEntity();
rec.setId(id);
- rec.setContent(content);
+ rec.content = content;
return rec;
}
@@ -104,8 +104,12 @@ public class RecordEntity extends AbstractEntity {
throw new IllegalArgumentException("Invalid record type: " + type + ". Must be one of: "
+ java.util.Arrays.toString(RecordType.values()), e);
}
- RecordEntity rec = build(name, recordType, ttl, content);
+ RecordEntity rec = new RecordEntity();
rec.setId(id);
+ rec.name = name;
+ rec.type = recordType.getType();
+ rec.ttl = ttl;
+ rec.content = content;
return rec;
}
diff --git a/src/main/java/codes/thischwa/cf/model/RecordType.java b/src/main/java/codes/thischwa/cf/model/RecordType.java
index 5fa911f..bff7a80 100644
--- a/src/main/java/codes/thischwa/cf/model/RecordType.java
+++ b/src/main/java/codes/thischwa/cf/model/RecordType.java
@@ -199,6 +199,6 @@ public enum RecordType {
@Override
public String toString() {
- return getType();
+ return type;
}
}
diff --git a/src/test/java/codes/thischwa/cf/CfClientPenTest.java b/src/test/java/codes/thischwa/cf/CfClientPenTest.java
index 890de9a..7110c3e 100644
--- a/src/test/java/codes/thischwa/cf/CfClientPenTest.java
+++ b/src/test/java/codes/thischwa/cf/CfClientPenTest.java
@@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
+import codes.thischwa.cf.auth.CfAuthBuilder;
import codes.thischwa.cf.model.RecordType;
import codes.thischwa.cf.model.ZoneEntity;
import java.util.List;
@@ -22,24 +23,22 @@ public class CfClientPenTest {
private static final String ZONE_STR = "mein-d-ns.de"; // existing baseline zone
- private static final String API_EMAIL = System.getenv("API_EMAIL");
- private static final String API_KEY = System.getenv("API_KEY");
+ private static final String API_TOKEN = System.getenv("API_TOKEN");
@BeforeAll
static void checkEnv() {
- assumeTrue(API_EMAIL != null && !API_EMAIL.isBlank(), "API_EMAIL not set; skipping pen tests");
- assumeTrue(API_KEY != null && !API_KEY.isBlank(), "API_KEY not set; skipping pen tests");
+ assumeTrue(API_TOKEN != null && !API_TOKEN.isBlank(), "API_TOKEN not set; skipping pen tests");
}
private CfDnsClient newClient() {
- return new CfDnsClient(true, API_EMAIL, API_KEY);
+ return new CfDnsClient(true, CfAuthBuilder.build(API_TOKEN));
}
@Test
@DisplayName("Invalid credentials should not authenticate and must throw CloudflareApiException")
void testInvalidCredentialsShouldFail() {
// Use syntactically valid but wrong credentials
- CfDnsClient badClient = new CfDnsClient("invalid@example.com", UUID.randomUUID().toString());
+ CfDnsClient badClient = new CfDnsClient(CfAuthBuilder.build("invalid@example.com", UUID.randomUUID().toString()));
assertThrows(CloudflareApiException.class, badClient::zoneList);
}
diff --git a/src/test/java/codes/thischwa/cf/CfClientTest.java b/src/test/java/codes/thischwa/cf/CfClientTest.java
index 70b5c28..632cd4d 100644
--- a/src/test/java/codes/thischwa/cf/CfClientTest.java
+++ b/src/test/java/codes/thischwa/cf/CfClientTest.java
@@ -8,6 +8,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
+import codes.thischwa.cf.auth.ApiTokenAuth;
+import codes.thischwa.cf.auth.CfAuthBuilder;
+import codes.thischwa.cf.auth.EmailKeyAuth;
import codes.thischwa.cf.model.BatchEntry;
import codes.thischwa.cf.model.RecordEntity;
import codes.thischwa.cf.model.RecordType;
@@ -28,15 +31,13 @@ public class CfClientTest {
private static final String SLD_STR = "devsld";
private static final int TTL = 60;
- private static final String API_EMAIL = System.getenv("API_EMAIL");
- private static final String API_KEY = System.getenv("API_KEY");
+ private static final String API_TOKEN = System.getenv("API_TOKEN");
- private final CfDnsClient client = new CfDnsClient(true, API_EMAIL, API_KEY);
+ private final CfDnsClient client = new CfDnsClient(true, CfAuthBuilder.build(API_TOKEN));
@BeforeAll
static void checkEnv() {
- assumeTrue(API_EMAIL != null && !API_EMAIL.isBlank(), "API_EMAIL not set; skipping pen tests");
- assumeTrue(API_KEY != null && !API_KEY.isBlank(), "API_KEY not set; skipping pen tests");
+ assumeTrue(API_TOKEN != null && !API_TOKEN.isBlank(), "API_TOKEN not set; skipping client tests");
}
@Test
@@ -106,13 +107,13 @@ public class CfClientTest {
void testDns() throws Exception {
// starting point: already existing zone 'mein-d-ns.de'
ZoneEntity z = client.zoneGet(ZONE_STR);
- assertEquals("0a83dd6e7f8c46039f2517bbded8115e", z.getId());
+ assertEquals("cf9d8b12f61423f280e0a3ea2a96d921", z.getId());
assertEquals("mein-d-ns.de", z.getName());
assertEquals("active", z.getStatus());
assertEquals(2, z.getNameServers().size());
- assertTrue(z.getNameServers().contains("sergi.ns.cloudflare.com"));
- assertEquals(4, z.getOriginalNameServers().size());
- assertTrue(z.getOriginalNameServers().contains("a.ns14.net"));
+ assertTrue(z.getNameServers().contains("rafe.ns.cloudflare.com"));
+ assertTrue(z.getOriginalNameServers().size() >= 2);
+ assertTrue(z.getOriginalNameServers().contains("blair.ns.cloudflare.com"));
assertNotNull(z.getActivatedOn());
assertNotNull(z.getModifiedOn());
assertNotNull(z.getCreatedOn());
@@ -226,10 +227,12 @@ public class CfClientTest {
@Test
void testException() {
- assertThrows(IllegalArgumentException.class, () -> new CfDnsClient(null, "key"));
- assertThrows(IllegalArgumentException.class, () -> new CfDnsClient("email", null));
- assertThrows(IllegalArgumentException.class, () -> new CfDnsClient("email", ""));
- assertThrows(IllegalArgumentException.class, () -> new CfDnsClient("", "key"));
+ // Test EmailKeyAuth validation
+ assertThrows(IllegalArgumentException.class, () -> new EmailKeyAuth("email", ""));
+ assertThrows(IllegalArgumentException.class, () -> new EmailKeyAuth("", "key"));
+
+ // Test ApiTokenAuth validation;
+ assertThrows(IllegalArgumentException.class, () -> new ApiTokenAuth(""));
}
@Test