diff --git a/build.gradle b/build.gradle index cf21acc..8c244fb 100644 --- a/build.gradle +++ b/build.gradle @@ -24,9 +24,13 @@ ext { junitVersion = "5.9.2" jacksonVersion = "2.15.3" slf4jVersion = "2.0.16" + lombokVersion = "1.18.34" } dependencies { + annotationProcessor("org.projectlombok:lombok:$lombokVersion") + compileOnly("org.projectlombok:lombok:$lombokVersion") + implementation("org.apache.httpcomponents.client5:httpclient5:5.5") implementation("org.apache.commons:commons-lang3:3.18.0") implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") diff --git a/src/main/java/ru/di9/ihc/DomainRecord.java b/src/main/java/ru/di9/ihc/DomainRecord.java new file mode 100644 index 0000000..4b7bd1c --- /dev/null +++ b/src/main/java/ru/di9/ihc/DomainRecord.java @@ -0,0 +1,49 @@ +package ru.di9.ihc; + +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class DomainRecord { + private final int id; + private final boolean readOnly; + private final RecordType type; + + private String name; + private String content; + private Integer priority; + + public DomainRecord(int id, boolean readOnly, String name, RecordType type, String content, Integer priority) { + this.id = id; + this.readOnly = readOnly; + this.name = name; + this.type = type; + this.content = content; + this.priority = priority; + } + + public void setName(String name) { + if (readOnly) { + throw new RuntimeException("READ ONLY RECORD"); + } + this.name = name; + } + + public void setContent(String content) { + if (readOnly) { + throw new RuntimeException("READ ONLY RECORD"); + } + this.content = content; + } + + public void setPriority(Integer priority) { + if (readOnly) { + throw new RuntimeException("READ ONLY RECORD"); + } + if (!RecordType.MX.equals(type) && !RecordType.SRV.equals(type)) { + throw new RuntimeException("NOT SUPPORT SET PRIORITY FOR " + type.name() + " RECORD"); + } + this.priority = priority; + } +} diff --git a/src/main/java/ru/di9/ihc/DomainRecordsResponse.java b/src/main/java/ru/di9/ihc/DomainRecordsResponse.java new file mode 100644 index 0000000..5e75e9d --- /dev/null +++ b/src/main/java/ru/di9/ihc/DomainRecordsResponse.java @@ -0,0 +1,22 @@ +package ru.di9.ihc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record DomainRecordsResponse(Data data) { + @JsonIgnoreProperties(ignoreUnknown = true) + public record Data(List records) { + } + + public record Record( + int id, + boolean readOnly, + String name, + String type, + String content, + Integer prio + ) { + } +} diff --git a/src/main/java/ru/di9/ihc/IhcClient.java b/src/main/java/ru/di9/ihc/IhcClient.java index 8b34fc4..213438f 100644 --- a/src/main/java/ru/di9/ihc/IhcClient.java +++ b/src/main/java/ru/di9/ihc/IhcClient.java @@ -16,9 +16,7 @@ import org.jsoup.select.Elements; import java.io.IOException; import java.net.URI; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import java.util.*; public class IhcClient { private final String baseUrl; @@ -90,4 +88,45 @@ public class IhcClient { throw new RuntimeException(e); } } + + public List getDomainRecords(int domainId) { + if (!isAuth) { + throw new RuntimeException("IS NOT AUTH"); + } + + HttpPost httpPost = new HttpPost(URI.create("%s/dnsZone/records".formatted(baseUrl))); + httpPost.setHeader("Accept", "application/json"); + httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); + httpPost.setHeader("Referer", "%s/dnsZone/index/%d".formatted(baseUrl, domainId)); + httpPost.setEntity(new UrlEncodedFormEntity(List.of( + new BasicNameValuePair("id", String.valueOf(domainId)) + ))); + + try { + return httpClient.execute(httpPost, resp -> { + if (resp.getCode() != 200) { + return Collections.emptyList(); + } + + DomainRecordsResponse response = mapper.readValue(resp.getEntity().getContent(), DomainRecordsResponse.class); + List list = new ArrayList<>(); + + for (DomainRecordsResponse.Record rec : response.data().records()) { + list.add(new DomainRecord( + rec.id(), + rec.readOnly(), + rec.name(), + RecordType.valueOf(rec.type()), + rec.content(), + rec.prio() + )); + } + + list.sort(Comparator.comparingInt(DomainRecord::getId)); + return list; + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/src/main/java/ru/di9/ihc/RecordType.java b/src/main/java/ru/di9/ihc/RecordType.java new file mode 100644 index 0000000..c05780a --- /dev/null +++ b/src/main/java/ru/di9/ihc/RecordType.java @@ -0,0 +1,13 @@ +package ru.di9.ihc; + +public enum RecordType { + SOA, + A, + AAAA, + CNAME, + MX, + TXT, + SRV, + NS, + CAA +} diff --git a/src/test/java/ru/di9/ihc/GetDomainRecordsTest.java b/src/test/java/ru/di9/ihc/GetDomainRecordsTest.java new file mode 100644 index 0000000..c4cfcdb --- /dev/null +++ b/src/test/java/ru/di9/ihc/GetDomainRecordsTest.java @@ -0,0 +1,79 @@ +package ru.di9.ihc; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.RandomUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static org.junit.jupiter.api.Assertions.*; + +class GetDomainRecordsTest { + static int port; + static WireMockServer wireMockServer; + + @BeforeAll + static void beforeAll() { + port = RandomUtils.nextInt(9000, 9999); + wireMockServer = new WireMockServer(port); + wireMockServer.start(); + } + + @AfterAll + static void afterAll() { + wireMockServer.stop(); + } + + @AfterEach + void after() { + wireMockServer.resetAll(); + } + + @Test + void test() throws IOException { + wireMockServer.stubFor(post("/j_spring_security_check?ajax=true") + .willReturn(WireMock.okJson(""" + {"redirect":{"url":"/"},"alert":{"type":"none","message":""}}"""))); + wireMockServer.stubFor(get("/dnsZone/list") + .willReturn(WireMock.ok(IOUtils.resourceToString("/ihc-dns.html", StandardCharsets.UTF_8)) + .withHeader("Content-Type", "text/html"))); + wireMockServer.stubFor(post("/dnsZone/records") + .willReturn(WireMock.okJson(IOUtils.resourceToString("/ihc-domain.json", StandardCharsets.UTF_8)))); + + var ihc = new IhcClient("http://localhost:%d".formatted(port)); + ihc.auth("user1", "passwd1"); + Integer domainId = ihc.getDomains().getFirst().getValue(); + List domainRecords = ihc.getDomainRecords(domainId); + + assertEquals(7, domainRecords.size()); + + DomainRecord record1 = domainRecords.getFirst(); + assertEquals(7000001, record1.getId()); + assertTrue(record1.isReadOnly()); + assertEquals(RecordType.SOA, record1.getType()); + assertEquals("", record1.getName()); + assertEquals("ns1.ihc.ru. info.ihc.ru. 2014120801 10800 3600 604800 3600", record1.getContent()); + assertNull(record1.getPriority()); + + assertThrows(RuntimeException.class, () -> record1.setName("xx")); + + DomainRecord record2 = domainRecords.get(4); + assertEquals(7000005, record2.getId()); + assertFalse(record2.isReadOnly()); + assertEquals(RecordType.MX, record2.getType()); + assertEquals("", record2.getName()); + assertEquals("mx.yandex.ru", record2.getContent()); + assertEquals(10, record2.getPriority()); + + assertDoesNotThrow(() -> record2.setName("xx")); + } +} diff --git a/src/test/resources/ihc-domain.json b/src/test/resources/ihc-domain.json new file mode 100644 index 0000000..bf5ea37 --- /dev/null +++ b/src/test/resources/ihc-domain.json @@ -0,0 +1,72 @@ +{ + "alert": { + "type": "none", + "message": "" + }, + "data": { + "errors": [], + "messages": [], + "domain": { + "name": "example-1.ru", + "id": 111111 + }, + "records": [ + { + "readOnly": true, + "id": 7000001, + "name": "", + "type": "SOA", + "content": "ns1.ihc.ru. info.ihc.ru. 2014120801 10800 3600 604800 3600", + "prio": null + }, + { + "readOnly": true, + "id": 7000002, + "name": "", + "type": "NS", + "content": "ns1.ihc.ru", + "prio": null + }, + { + "readOnly": true, + "id": 7000003, + "name": "", + "type": "NS", + "content": "ns2.ihc.ru", + "prio": null + }, + { + "readOnly": false, + "id": 7000004, + "name": "yamail-XXXXXXXXXXXX", + "type": "CNAME", + "content": "mail.yandex.ru", + "prio": null + }, + { + "readOnly": false, + "id": 7000005, + "name": "", + "type": "MX", + "content": "mx.yandex.ru", + "prio": 10 + }, + { + "readOnly": false, + "id": 7000006, + "name": "", + "type": "A", + "content": "127.0.0.1", + "prio": null + }, + { + "readOnly": false, + "id": 7000007, + "name": "_acme-challenge", + "type": "TXT", + "content": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "prio": null + } + ] + } +}