Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 62b86ca22f | |||
| f12f5d6db1 | |||
| c435a80966 | |||
| 1e8fe53891 | |||
| 10ad4bd7f3 | |||
| 84a454f656 | |||
| 4a3ec42fa5 | |||
| 9e09b12dc3 | |||
| d0802fc01d | |||
| 607b634b35 | |||
| b593ca7311 | |||
| 773573bd39 | |||
| b890ae9d17 |
+1
-1
@@ -32,7 +32,7 @@ build:
|
||||
- mkdir public
|
||||
- cp -rv docs/* public/
|
||||
- mkdir public/apidocs
|
||||
- cp -rv target/apidocs public/
|
||||
- cp -rv target/reports/apidocs public/
|
||||
artifacts:
|
||||
paths:
|
||||
- target/surefire-reports/*.xml
|
||||
|
||||
@@ -29,7 +29,7 @@ This guide comes without any warranty. Use at your own risk. The author is not r
|
||||
|
||||
The project has its own maven repository. It can be added to the `pom.xml`:
|
||||
|
||||
```xml
|
||||
```xml<p>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>gitlab-cloudflare</id>
|
||||
@@ -49,14 +49,18 @@ The dependency is:
|
||||
|
||||
## Changelog
|
||||
|
||||
- 0.2.0-beta-SNAPSHOT:
|
||||
- **New Fluent API**: Added chainable method interface for more readable DNS operations (
|
||||
`client.zone().record()...`)
|
||||
- 0.3.0-SNAPSHOT:
|
||||
- **Breaking Change**:
|
||||
- **New Fluent API**: Changed the initialization of the client(`new CfDnsClientBuilder().withApiTokenAuth("your-api-token").build()`)
|
||||
- Authentication with API token.
|
||||
- 0.2.0:
|
||||
- **Breaking Change**: `emptyResultThrowsException` default changed from `true` to `false`. Now applies to both
|
||||
single and multiple result requests. Empty results will be returned by default without throwing exceptions.
|
||||
- API method names refactored for consistency: `zoneListAll` → `zoneList`, `zoneInfo` → `zoneGet`, `sldListAll` →
|
||||
`recordList`
|
||||
- RecordEntity getter methods renamed for clarity: `getName()` → `getSld()`
|
||||
- **New Fluent API**: Changed the initialization of the client(`new CfDnsClientBuilder().withApiTokenAuth("your-api-token").build()`) and added chainable method interface for more readable DNS operations (
|
||||
`client.zone().record()...`)
|
||||
- Code quality improvements: eliminated duplication in batch operations, improved type safety in HTTP methods,
|
||||
optimized string concatenation, removed mutable setters from CfDnsClient
|
||||
- Enhanced type validation in `RecordEntity.build()` with better error messages
|
||||
@@ -88,10 +92,18 @@ the [javadoc of the CfDnsClient](https://cloudflaredns-java-f4ee3a.gitlab.io/api
|
||||
|
||||
### Instantiation of `CfDnsClient`
|
||||
|
||||
#### With API Token (recommended):
|
||||
```java
|
||||
CfDnsClient cfDnsClient = new CfDnsClient(
|
||||
"email@example.com", "yourApiKey"
|
||||
);
|
||||
CfDnsClient cfDnsClient = new CfDnsClientBuilder()
|
||||
.withApiTokenAuth("your-api-token")
|
||||
.build();
|
||||
```
|
||||
|
||||
#### With Email/Key (legacy):
|
||||
```java
|
||||
CfDnsClient cfDnsClient = new CfDnsClientBuilder()
|
||||
.withEmailKeyAuth("email@example.com", "yourApiKey")
|
||||
.build();
|
||||
```
|
||||
|
||||
### `zoneList`
|
||||
@@ -389,7 +401,9 @@ client.zone("example.com")
|
||||
### Complete Example
|
||||
|
||||
```java
|
||||
CfDnsClient client = new CfDnsClient("email@example.com", "yourApiKey");
|
||||
CfDnsClient client = new CfDnsClientBuilder()
|
||||
.withApiTokenAuth("your-api-token")
|
||||
.build();
|
||||
|
||||
// Create a new record
|
||||
client.zone("example.com")
|
||||
@@ -424,7 +438,10 @@ The `CfDnsClient` provides internal error-handling mechanisms through exceptions
|
||||
To enable exception throwing for empty results:
|
||||
|
||||
```java
|
||||
CfDnsClient client = new CfDnsClient(true, "email@example.com", "yourApiKey");
|
||||
CfDnsClient client = new CfDnsClientBuilder()
|
||||
.withApiTokenAuth("your-api-token")
|
||||
.withEmptyResultThrowsException(true)
|
||||
.build();
|
||||
```
|
||||
|
||||
## Example:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<groupId>codes.thischwa</groupId>
|
||||
<artifactId>cloudflaredns</artifactId>
|
||||
<version>0.2.0</version>
|
||||
<version>0.3.0</version>
|
||||
<name>CloudflareDNS-java</name>
|
||||
<inceptionYear>2025</inceptionYear>
|
||||
<packaging>jar</packaging>
|
||||
@@ -21,24 +21,26 @@
|
||||
<project.reporting.outputEncoding>${file.encoding}</project.reporting.outputEncoding>
|
||||
|
||||
<!-- checkstyle -->
|
||||
<checkstyle.version>10.21.3</checkstyle.version>
|
||||
<checkstyle.version>12.3.0</checkstyle.version>
|
||||
<checkstyle.plugin.version>3.6.0</checkstyle.plugin.version>
|
||||
<checkstyle.config.location>${project.basedir}/src/checkstyle/google_custom_checks.xml
|
||||
</checkstyle.config.location>
|
||||
<checkstyle.includeTestResources>false</checkstyle.includeTestResources>
|
||||
<checkstyle.violationSeverity>warning</checkstyle.violationSeverity>
|
||||
<checkstyle.failOnViolation>false</checkstyle.failOnViolation>
|
||||
<checkstyle.propertyExpansion>config_loc=${basedir}
|
||||
</checkstyle.propertyExpansion>
|
||||
<checkstyle.consoleOutput>true</checkstyle.consoleOutput>
|
||||
<linkX-Ref>false</linkX-Ref>
|
||||
<linkXRef>false</linkXRef>
|
||||
|
||||
<!-- 3rd party dependencies -->
|
||||
<jackson.version>2.18.2</jackson.version>
|
||||
<httpclient5.version>5.4.3</httpclient5.version>
|
||||
<jackson.version>2.19.1</jackson.version>
|
||||
<httpclient5.version>5.5.1</httpclient5.version>
|
||||
<lombok.version>1.18.36</lombok.version>
|
||||
<slf4j.version>2.0.17</slf4j.version>
|
||||
<logback-classic.version>1.5.18</logback-classic.version>
|
||||
<junit5.version>5.12.2</junit5.version>
|
||||
<mockito-junit5.version>5.17.0</mockito-junit5.version>
|
||||
<junit5.version>5.14.2</junit5.version>
|
||||
<mockito-junit5.version>5.21.0</mockito-junit5.version>
|
||||
|
||||
<!-- sonarqube -->
|
||||
<sonar.organization>th-schwarz</sonar.organization>
|
||||
@@ -56,7 +58,7 @@
|
||||
<developerConnection>scm:git:git@gitlab.com:th-schwarz/CloudflareDNS-java.git</developerConnection>
|
||||
<connection>scm:git:git@gitlab.com:th-schwarz/CloudflareDNS-java.git</connection>
|
||||
<url>https://gitlab.com/th-schwarz/CloudflareDNS-java</url>
|
||||
<tag>v0.2.0</tag>
|
||||
<tag>v0.3.0</tag>
|
||||
</scm>
|
||||
|
||||
<distributionManagement>
|
||||
@@ -149,7 +151,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.6.3</version>
|
||||
<version>3.11.2</version>
|
||||
<configuration>
|
||||
<failOnError>false</failOnError>
|
||||
<locale>en</locale>
|
||||
@@ -168,7 +170,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-release-plugin</artifactId>
|
||||
<version>3.1.1</version>
|
||||
<version>3.3.1</version>
|
||||
<configuration>
|
||||
<tagNameFormat>v@{project.version}</tagNameFormat>
|
||||
<arguments>-DskipTests</arguments>
|
||||
@@ -202,7 +204,7 @@
|
||||
<!-- generates the code coverage report for sonar cube -->
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>0.8.12</version>
|
||||
<version>0.8.13</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prepare-agent</id>
|
||||
@@ -261,6 +263,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.4.2</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-delomboked-sources</id>
|
||||
|
||||
@@ -19,6 +19,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 +28,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 CfDnsClientBuilder.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 CfDnsClientBuilder.CfAuth auth) {
|
||||
this.baseUrl = baseUrl;
|
||||
this.authEmail = authEmail;
|
||||
this.authKey = authKey;
|
||||
this.auth = auth;
|
||||
this.objectMapper = JsonConf.initObjectMapper();
|
||||
}
|
||||
|
||||
@@ -53,8 +51,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();
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,10 @@ import codes.thischwa.cf.model.ZoneMultipleResponse;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -26,74 +28,59 @@ import org.jetbrains.annotations.Nullable;
|
||||
* records and zones within the Cloudflare system, including creating, updating, retrieving, and
|
||||
* deleting DNS records.
|
||||
*
|
||||
* <p>Example:
|
||||
* <p>Example with API token authentication (recommended):
|
||||
* <pre><code>
|
||||
* // Create a new CfDnsClient instance
|
||||
* CfDnsClient cfDnsClient = new CfDnsClient(
|
||||
* "email@example.com",
|
||||
* "yourApiKey"
|
||||
* );
|
||||
* // Create a new CfDnsClient instance with API token
|
||||
* CfDnsClient cfDnsClient = new CfDnsClientBuilder()
|
||||
* .withApiTokenAuth("your-api-token")
|
||||
* .build();
|
||||
*
|
||||
* // 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());
|
||||
* </code></pre>
|
||||
*
|
||||
* <p>Example with email/key authentication (legacy):
|
||||
* <pre><code>
|
||||
* CfDnsClient cfDnsClient = new CfDnsClientBuilder()
|
||||
* .withEmailKeyAuth("email@example.com", "your-api-key")
|
||||
* .build();
|
||||
* </code></pre>
|
||||
*
|
||||
* <p>Example with exception throwing enabled:
|
||||
* <pre><code>
|
||||
* // Throws exception when results are empty
|
||||
* CfDnsClient cfDnsClient = new CfDnsClientBuilder()
|
||||
* .withApiTokenAuth("your-api-token")
|
||||
* .withEmptyResultThrowsException(true)
|
||||
* .build();
|
||||
* </code></pre>
|
||||
*
|
||||
* <p>Example with custom base URL:
|
||||
* <pre><code>
|
||||
* CfDnsClient cfDnsClient = new CfDnsClientBuilder()
|
||||
* .withApiTokenAuth("your-api-token")
|
||||
* .withBaseUrl("https://custom-api.example.com")
|
||||
* .build();
|
||||
* </code></pre>
|
||||
*/
|
||||
@Slf4j
|
||||
public class CfDnsClient extends CfBasicHttpClient {
|
||||
private 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}.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
public CfDnsClient(String authEmail, String authKey) {
|
||||
this(DEFAULT_BASEURL, authEmail, authKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public CfDnsClient(String baseUrl, String authEmail, String authKey) {
|
||||
this(false, baseUrl, authEmail, authKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new instance of {@code CfDnsClient}.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
public CfDnsClient(boolean emptyResultThrowsException, String authEmail, String authKey) {
|
||||
this(emptyResultThrowsException, DEFAULT_BASEURL, authEmail, authKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new instance of {@code CfDnsClient}.
|
||||
*
|
||||
@@ -101,14 +88,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);
|
||||
CfDnsClient(boolean emptyResultThrowsException, String baseUrl, CfDnsClientBuilder.CfAuth auth) {
|
||||
super(baseUrl, auth);
|
||||
this.responseValidator = new ResponseValidator(emptyResultThrowsException);
|
||||
this.emptyResultThrowsException = emptyResultThrowsException;
|
||||
}
|
||||
@@ -316,9 +299,9 @@ public class CfDnsClient extends CfBasicHttpClient {
|
||||
RecordSingleResponse resp = postRequest(endpoint, rec, RecordSingleResponse.class);
|
||||
checkResponse(resp);
|
||||
log.info("Record {} of type {} successful created.", rec.getSld(), rec.getType());
|
||||
RecordEntity record = resp.getResult();
|
||||
record.setZoneId(zone.getId());
|
||||
return record;
|
||||
RecordEntity retRec = resp.getResult();
|
||||
retRec.setZoneId(zone.getId());
|
||||
return retRec;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -452,7 +435,10 @@ public class CfDnsClient extends CfBasicHttpClient {
|
||||
throws CloudflareNotFoundException {
|
||||
List<RecordEntity> filtered;
|
||||
if (types != null && types.length > 0) {
|
||||
filtered = recs.stream().filter(rec -> Arrays.asList(types).contains(RecordType.valueOf(rec.getType()))).collect(Collectors.toList());
|
||||
Set<RecordType> allowedTypes = new HashSet<>(Arrays.asList(types));
|
||||
filtered = recs.stream()
|
||||
.filter(rec -> allowedTypes.contains(RecordType.valueOf(rec.getType())))
|
||||
.collect(Collectors.toList());;
|
||||
} else {
|
||||
filtered = new ArrayList<>(recs);
|
||||
}
|
||||
@@ -506,4 +492,5 @@ public class CfDnsClient extends CfBasicHttpClient {
|
||||
throws CloudflareApiException {
|
||||
responseValidator.validate(resp, singleResultExpected);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
package codes.thischwa.cf;
|
||||
|
||||
import org.apache.hc.core5.http.ClassicHttpRequest;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Builder class for configuring and creating instances of {@link CfDnsClient}.
|
||||
* This class provides a fluent API for customizing the client settings,
|
||||
* such as the base URL and authentication mechanism.
|
||||
*/
|
||||
public class CfDnsClientBuilder {
|
||||
|
||||
/**
|
||||
* The default base URL for the Cloudflare v4 API requests made by the {@code CfDnsClient}.
|
||||
*/
|
||||
public static final String DEFAULT_BASEURL = "https://api.cloudflare.com/client/v4";
|
||||
|
||||
private boolean emptyResultThrowsException;
|
||||
private CfAuth auth;
|
||||
|
||||
@Nullable
|
||||
private String baseUrl;
|
||||
|
||||
/**
|
||||
* Constructs a new instance of `CfDnsClientBuilder`.
|
||||
*
|
||||
* <p>This class serves as a builder for creating and configuring instances of a CfDnsClient. It provides
|
||||
* a fluent API to set various optional configurations, such as API authentication methods and base
|
||||
* URL, before constructing the client.
|
||||
*
|
||||
* <p>By using this constructor, you can initiate the building process with default settings, which can
|
||||
* later be overridden using the provided builder methods.
|
||||
*/
|
||||
public CfDnsClientBuilder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures whether an exception should be thrown when an empty result is encountered
|
||||
* during operations performed by the `CfDnsClient`.
|
||||
*
|
||||
* @param emptyResultThrowsException a boolean flag indicating if an exception should be thrown
|
||||
* when an empty result is returned. If set to `true`, operations
|
||||
* that result in an empty response will throw an exception;
|
||||
* otherwise, they will not.
|
||||
* @return the current instance of {@code CfDnsClientBuilder}, allowing for method chaining
|
||||
* to further configure the builder.
|
||||
*/
|
||||
public CfDnsClientBuilder withEmptyResultThrowsException(boolean emptyResultThrowsException) {
|
||||
this.emptyResultThrowsException = emptyResultThrowsException;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the base URL to be used by the {@code CfDnsClient}.
|
||||
* This method allows configuring the base URL for API requests, overriding any default value.
|
||||
*
|
||||
* @param baseUrl the base URL to be used for API requests
|
||||
* @return the current instance of {@code CfDnsClientBuilder}, enabling method chaining
|
||||
*/
|
||||
public CfDnsClientBuilder withBaseUrl(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the authentication method for the {@code CfDnsClient} to use an API token.
|
||||
* This is the recommended way to authenticate with the Cloudflare API, as it provides
|
||||
* enhanced security and ease of use compared to other authentication mechanisms.
|
||||
*
|
||||
* @param apiToken the Cloudflare API token. This token is required for authenticating
|
||||
* API requests and must not be null or blank.
|
||||
* @return the current instance of {@code CfDnsClientBuilder}, allowing for method chaining
|
||||
* to further configure the builder.
|
||||
* @throws IllegalArgumentException if the {@code apiToken} is null or blank.
|
||||
*/
|
||||
public CfDnsClientBuilder withApiTokenAuth(String apiToken) {
|
||||
this.auth = new ApiTokenAuth(apiToken);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the authentication method for the {@code CfDnsClient} to use an email and API key.
|
||||
* This approach uses the legacy authentication mechanism provided by Cloudflare, where requests
|
||||
* are authenticated with a combination of an account's email address and API key.
|
||||
*
|
||||
* @param authEmail the email address associated with the Cloudflare account. This must not be null or blank.
|
||||
* @param authKey the API key of the Cloudflare account. This must not be null or blank.
|
||||
* @return the current instance of {@code CfDnsClientBuilder}, allowing for method chaining
|
||||
* to further configure the builder.
|
||||
* @throws IllegalArgumentException if {@code authEmail} or {@code authKey} is null or blank.
|
||||
*/
|
||||
public CfDnsClientBuilder withEmailKeyAuth(String authEmail, String authKey) {
|
||||
this.auth = new EmailKeyAuth(authEmail, authKey);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns a configured instance of {@code CfDnsClient}.
|
||||
*
|
||||
* <p>The method constructs a new {@code CfDnsClient} object based on the
|
||||
* options set in the {@code CfDnsClientBuilder}. If no base URL has been
|
||||
* explicitly configured, a default base URL will be used.
|
||||
*
|
||||
* @return a new instance of {@code CfDnsClient} configured with the
|
||||
* specified options such as base URL, authentication details,
|
||||
* and the exception-handling policy for empty results.
|
||||
*/
|
||||
public CfDnsClient build() {
|
||||
String url = baseUrl == null ? DEFAULT_BASEURL : baseUrl;
|
||||
return new CfDnsClient(emptyResultThrowsException, url, auth);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
interface CfAuth {
|
||||
|
||||
/**
|
||||
* Applies authentication headers to the given HTTP request.
|
||||
*
|
||||
* @param request the HTTP request to authenticate
|
||||
*/
|
||||
void applyAuth(ClassicHttpRequest request);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication mechanism using Cloudflare API token.
|
||||
* This is the recommended authentication method for the Cloudflare API.
|
||||
*/
|
||||
static 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
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication mechanism using Cloudflare account email and API key.
|
||||
* This is the legacy authentication method for the Cloudflare API.
|
||||
*/
|
||||
static 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
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,17 +46,27 @@ class ResponseValidator {
|
||||
private void validateResultCount(AbstractResponse resp, boolean singleResultExpected)
|
||||
throws CloudflareApiException {
|
||||
if (resp instanceof RecordMultipleResponse respMulti) {
|
||||
if (singleResultExpected && respMulti.getResultInfo().totalCount() > 1) {
|
||||
throw new CloudflareApiException(
|
||||
"Unexpected result count: " + respMulti.getResultInfo().totalCount());
|
||||
}
|
||||
if (emptyResultThrowsException && respMulti.getResultInfo().totalCount() == 0) {
|
||||
throw new CloudflareNotFoundException("No result found");
|
||||
}
|
||||
validateMultipleResponse(respMulti, singleResultExpected);
|
||||
} else if (resp instanceof AbstractSingleResponse<?> respSingle) {
|
||||
if (emptyResultThrowsException && respSingle.getResult() == null) {
|
||||
throw new CloudflareNotFoundException("No result found");
|
||||
}
|
||||
validateSingleResponse(respSingle);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateMultipleResponse(RecordMultipleResponse response, boolean singleResultExpected)
|
||||
throws CloudflareApiException {
|
||||
int totalCount = response.getResultInfo().totalCount();
|
||||
if (singleResultExpected && totalCount > 1) {
|
||||
throw new CloudflareApiException("Unexpected result count: " + totalCount);
|
||||
}
|
||||
if (emptyResultThrowsException && totalCount == 0) {
|
||||
throw new CloudflareNotFoundException("No result found");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateSingleResponse(AbstractSingleResponse<?> response)
|
||||
throws CloudflareNotFoundException {
|
||||
if (emptyResultThrowsException && response.getResult() == null) {
|
||||
throw new CloudflareNotFoundException("No result found");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,16 +45,16 @@ public class RecordOperationsImpl implements RecordOperations {
|
||||
|
||||
@Override
|
||||
public RecordEntity update(String newContent) throws CloudflareApiException {
|
||||
List<RecordEntity> records = get();
|
||||
if (records.isEmpty()) {
|
||||
throw new CloudflareApiException("No records found to update for subdomain: " + sld);
|
||||
List<RecordEntity> recs = get();
|
||||
if (recs.isEmpty()) {
|
||||
throw new CloudflareApiException("No recs found to update for subdomain: " + sld);
|
||||
}
|
||||
if (records.size() > 1) {
|
||||
throw new CloudflareApiException("Multiple records found. Please use recordUpdate() directly for precise control.");
|
||||
if (recs.size() > 1) {
|
||||
throw new CloudflareApiException("Multiple recs found. Please use recordUpdate() directly for precise control.");
|
||||
}
|
||||
RecordEntity record = records.get(0);
|
||||
record.setContent(newContent);
|
||||
return client.recordUpdate(zone, record);
|
||||
RecordEntity rec = recs.get(0);
|
||||
rec.setContent(newContent);
|
||||
return client.recordUpdate(zone, rec);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -199,6 +199,6 @@ public enum RecordType {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getType();
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,12 @@ package codes.thischwa.cf.model;
|
||||
* <p>This class contains information about the current page, page size, total pages, and result
|
||||
* counts, which can be utilized in managing and navigating through paginated data.
|
||||
*
|
||||
* <ul>
|
||||
* <li><b>page:</b> The current page number.
|
||||
* <li><b>perPage:</b> The number of results per page.
|
||||
* <li><b>totalPages:</b> The total number of pages available.
|
||||
* <li><b>count:</b> The number of results on the current page.
|
||||
* <li><b>totalCount:</b> The total number of results across all pages.
|
||||
* </ul>
|
||||
* @param page The current page number.
|
||||
* @param perPage The number of results per page.
|
||||
* @param totalPages The total number of pages available.
|
||||
* @param count The number of results on the current page.
|
||||
* @param totalCount The total number of results across all pages.
|
||||
*/
|
||||
|
||||
public record ResultInfo(int page, int perPage, int totalPages, int count, int totalCount) {
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,24 +22,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 CfDnsClientBuilder().withEmptyResultThrowsException(true).withApiTokenAuth(API_TOKEN).build();
|
||||
}
|
||||
|
||||
@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 CfDnsClientBuilder().withEmailKeyAuth("invalid@example.com", UUID.randomUUID().toString()).build();
|
||||
assertThrows(CloudflareApiException.class, badClient::zoneList);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,15 +28,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 CfDnsClientBuilder().withEmptyResultThrowsException(true).withApiTokenAuth(API_TOKEN).build();
|
||||
|
||||
@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 +104,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());
|
||||
@@ -224,14 +222,6 @@ 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
|
||||
void testRecordEntityInvalidType() {
|
||||
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
|
||||
|
||||
Reference in New Issue
Block a user