diff --git a/cli/supportedProviders.txt b/cli/supportedProviders.txt index 7c56aad2..ec68b99d 100644 --- a/cli/supportedProviders.txt +++ b/cli/supportedProviders.txt @@ -7,4 +7,5 @@ denominator.designate.DesignateProvider denominator.dynect.DynECTProvider denominator.mock.MockProvider denominator.route53.Route53Provider -denominator.ultradns.UltraDNSProvider \ No newline at end of file +denominator.ultradns.UltraDNSProvider +denominator.verisigndns.VerisignDnsProvider \ No newline at end of file diff --git a/model/src/test/java/denominator/assertj/ModelAssertions.java b/model/src/test/java/denominator/assertj/ModelAssertions.java index ac47e342..3fca70bc 100644 --- a/model/src/test/java/denominator/assertj/ModelAssertions.java +++ b/model/src/test/java/denominator/assertj/ModelAssertions.java @@ -7,7 +7,7 @@ public class ModelAssertions extends Assertions { - public static ResourceRecordSetAssert assertThat(ResourceRecordSet actual) { + public static ResourceRecordSetAssert assertThat(ResourceRecordSet actual) { return new ResourceRecordSetAssert(actual); } diff --git a/settings.gradle b/settings.gradle index b3b967ff..31a48d0c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,6 @@ rootProject.name='denominator' -include 'model', 'core', 'route53', 'ultradns', 'dynect', 'clouddns', 'designate', 'cli' +include 'model', 'core', 'route53', 'ultradns', 'verisigndns', 'dynect', 'clouddns', 'designate', 'cli' rootProject.children.each { childProject -> childProject.name = 'denominator-' + childProject.name diff --git a/verisigndns/README.md b/verisigndns/README.md new file mode 100644 index 00000000..6c094449 --- /dev/null +++ b/verisigndns/README.md @@ -0,0 +1,5 @@ +## Notable Behaviors +The following are notable when compared to different providers. +* `Zone.id()` is the `Zone.name()` +* Zone lists are 1 + N requests in order to zip with the SOA's ttl and rname. +* The default ttl for record sets is 86400. diff --git a/verisigndns/build.gradle b/verisigndns/build.gradle new file mode 100644 index 00000000..38db092b --- /dev/null +++ b/verisigndns/build.gradle @@ -0,0 +1,21 @@ +apply plugin: 'java' + +sourceCompatibility = 1.6 + +test { + systemProperty 'verisigndns.url', System.getProperty('verisignmdns.url', '') + systemProperty 'verisigndns.username', System.getProperty('verisignmdns.username', '') + systemProperty 'verisigndns.password', System.getProperty('verisignmdns.password', '') + systemProperty 'verisigndns.zone', System.getProperty('verisignmdns.zone', '') +} + +dependencies { + compile project(':denominator-core') + compile 'com.netflix.feign:feign-core:8.10.0' + compile 'com.netflix.feign:feign-sax:8.10.0' + testCompile project(':denominator-model').sourceSets.test.output + testCompile project(':denominator-core').sourceSets.test.output + testCompile 'junit:junit:4.12' + testCompile 'org.assertj:assertj-core:1.7.1' // last version supporting JDK 7 + testCompile 'com.squareup.okhttp:mockwebserver:2.5.0' +} diff --git a/verisigndns/src/main/java/denominator/verisigndns/HostedZonesReadable.java b/verisigndns/src/main/java/denominator/verisigndns/HostedZonesReadable.java new file mode 100644 index 00000000..0634b129 --- /dev/null +++ b/verisigndns/src/main/java/denominator/verisigndns/HostedZonesReadable.java @@ -0,0 +1,31 @@ +package denominator.verisigndns; + +import javax.inject.Inject; + +import denominator.CheckConnection; +import denominator.verisigndns.VerisignDnsEncoder.Paging; + +public class HostedZonesReadable implements CheckConnection { + + private final VerisignDns api; + + @Inject + HostedZonesReadable(VerisignDns api) { + this.api = api; + } + + @Override + public boolean ok() { + try { + api.getZones(new Paging(1, 1)); + return true; + } catch (RuntimeException e) { + return false; + } + } + + @Override + public String toString() { + return "HostedZonesReadable"; + } +} diff --git a/verisigndns/src/main/java/denominator/verisigndns/ResourceRecordByNameAndTypeIterator.java b/verisigndns/src/main/java/denominator/verisigndns/ResourceRecordByNameAndTypeIterator.java new file mode 100644 index 00000000..b0c79fc4 --- /dev/null +++ b/verisigndns/src/main/java/denominator/verisigndns/ResourceRecordByNameAndTypeIterator.java @@ -0,0 +1,121 @@ +package denominator.verisigndns; + +import static denominator.common.Util.peekingIterator; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import denominator.common.PeekingIterator; +import denominator.common.Util; +import denominator.model.ResourceRecordSet; +import denominator.model.ResourceRecordSet.Builder; +import denominator.verisigndns.VerisignDnsContentHandlers.Page; +import denominator.verisigndns.VerisignDnsContentHandlers.ResourceRecord; +import denominator.verisigndns.VerisignDnsEncoder.GetRRList; + +final class ResourceRecordByNameAndTypeIterator implements Iterator> { + + private final VerisignDns api; + private final GetRRList getRRList; + private final String zoneSuffix; + private PeekingIterator peekingIterator; + + public ResourceRecordByNameAndTypeIterator(VerisignDns api, GetRRList getRRList) { + this.api = api; + this.getRRList = getRRList; + zoneSuffix = "." + getRRList.getZoneName() + "."; + } + + @Override + public boolean hasNext() { + if (peekingIterator == null || !peekingIterator.hasNext()) { + nextPeekingIterator(); + } + return peekingIterator.hasNext(); + } + + private void nextPeekingIterator() { + if (getRRList.nextPage()) { + Page rrPage = api.getResourceRecords(getRRList.getZoneName(), getRRList); + getRRList.setTotal(rrPage.getCount()); + peekingIterator = peekingIterator(rrPage.getList().iterator()); + } + } + + private String relativeName(String name, String root) { + if (name.endsWith(root)) { + name = name.substring(0, name.length() - root.length()); + } + return name; + } + + @Override + public ResourceRecordSet next() { + if (peekingIterator == null) { + nextPeekingIterator(); + } + ResourceRecord record = peekingIterator.next(); + if (record == null) { + return null; + } + + String owner = relativeName(record.getName(), zoneSuffix); + String type = record.getType(); + Builder> builder = + ResourceRecordSet.builder().name(owner).type(type).ttl(record.getTtl()); + builder.add(getRRTypeAndRdata(type, record.getRdata())); + + while (hasNext()) { + ResourceRecord next = peekingIterator.peek(); + if (fqdnAndTypeEquals(next, record)) { + peekingIterator.next(); + builder.add(getRRTypeAndRdata(type, next.getRdata())); + } else { + break; + } + } + return builder.build(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + private static boolean fqdnAndTypeEquals(ResourceRecord actual, ResourceRecord expected) { + return actual.getName().equals(expected.getName()) && actual.getType().equals(expected.getType()); + } + + private static final int NAPTR_FIELD_FLAGS = 2; + private static final int NAPTR_FIELD_SERVICE = 3; + + private static Map getRRTypeAndRdata(String type, String rdata) { + + rdata = rdata.replace("\"", ""); + try { + if ("AAAA".equals(type)) { + rdata = rdata.toUpperCase(); + } else if ("NAPTR".equals(type)) { + List parts = Util.split(' ', rdata); + + if (parts.size() > NAPTR_FIELD_SERVICE) { + parts.set(NAPTR_FIELD_FLAGS, parts.get(NAPTR_FIELD_FLAGS).toUpperCase()); + + String service = parts.get(NAPTR_FIELD_SERVICE); + List serviceParts = Util.split('+', service); + serviceParts.set(0, serviceParts.get(0).toUpperCase()); + parts.set(NAPTR_FIELD_SERVICE, Util.join('+', serviceParts.toArray())); + + rdata = Util.join(' ', parts.toArray()); + } + } + return Util.toMap(type, rdata); + } catch (IllegalArgumentException e) { + Map map = new LinkedHashMap(); + map.put(type, rdata); + return map; + } + } +} diff --git a/verisigndns/src/main/java/denominator/verisigndns/VerisignDns.java b/verisigndns/src/main/java/denominator/verisigndns/VerisignDns.java new file mode 100644 index 00000000..ec2e4bfd --- /dev/null +++ b/verisigndns/src/main/java/denominator/verisigndns/VerisignDns.java @@ -0,0 +1,44 @@ +package denominator.verisigndns; + +import denominator.model.ResourceRecordSet; +import denominator.model.Zone; +import denominator.verisigndns.VerisignDnsContentHandlers.Page; +import denominator.verisigndns.VerisignDnsContentHandlers.ResourceRecord; +import denominator.verisigndns.VerisignDnsEncoder.GetRRList; +import denominator.verisigndns.VerisignDnsEncoder.Paging; +import feign.Param; +import feign.RequestLine; + +interface VerisignDns { + + @RequestLine("POST") + void createZone(@Param("createZone") Zone zone); + + @RequestLine("POST") + void updateSoa(@Param("updateSoa") Zone zone); + + @RequestLine("POST") + void deleteZone(@Param("deleteZone") String zone); + + @RequestLine("POST") + Page getZones(@Param("getZoneList") Paging paging); + + @RequestLine("POST") + Zone getZone(@Param("getZone") String zone); + + @RequestLine("POST") + void createResourceRecords(@Param("zone") String zone, + @Param("rrSet") ResourceRecordSet rrSet, @Param("oldRRSet") ResourceRecordSet oldRRSet); + + @RequestLine("POST") + void updateResourceRecords(@Param("zone") String zone, + @Param("rrSet") ResourceRecordSet rrSet, @Param("oldRRSet") ResourceRecordSet oldRRSet); + + @RequestLine("POST") + Page getResourceRecords(@Param("zone") String zone, + @Param("getRRList") GetRRList rrRequest); + + @RequestLine("POST") + void deleteResourceRecords(@Param("zone") String zone, + @Param("deleteRRSet") ResourceRecordSet rrSet); +} diff --git a/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsAllProfileResourceRecordSetApi.java b/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsAllProfileResourceRecordSetApi.java new file mode 100644 index 00000000..2c9edd16 --- /dev/null +++ b/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsAllProfileResourceRecordSetApi.java @@ -0,0 +1,176 @@ +package denominator.verisigndns; + +import static denominator.common.Preconditions.checkArgument; +import static denominator.common.Preconditions.checkNotNull; +import static denominator.common.Util.equal; +import static denominator.common.Util.nextOrNull; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; + +import denominator.AllProfileResourceRecordSetApi; +import denominator.model.ResourceRecordSet; +import denominator.verisigndns.VerisignDnsEncoder.GetRRList; + +final class VerisignDnsAllProfileResourceRecordSetApi implements AllProfileResourceRecordSetApi { + + private final VerisignDns api; + private final String zoneName; + + VerisignDnsAllProfileResourceRecordSetApi(VerisignDns api, String zoneName) { + this.api = api; + this.zoneName = zoneName; + } + + @Override + public Iterator> iterator() { + GetRRList getRRList = new GetRRList(zoneName); + + return new ResourceRecordByNameAndTypeIterator(api, getRRList); + } + + @Override + public Iterator> iterateByName(String name) { + checkNotNull(name, "name"); + + GetRRList getRRList = new GetRRList(zoneName, name); + + return new ResourceRecordByNameAndTypeIterator(api, getRRList); + } + + @Override + public Iterator> iterateByNameAndType(String name, String type) { + checkNotNull(name, "name"); + checkNotNull(type, "type"); + + GetRRList getRRList = new GetRRList(zoneName, name, type); + + return new ResourceRecordByNameAndTypeIterator(api, getRRList); + } + + @Override + public ResourceRecordSet getByNameTypeAndQualifier(String name, String type, String qualifier) { + checkNotNull(name, "name"); + checkNotNull(type, "type"); + checkNotNull(qualifier, "qualifier"); + + GetRRList getRRList = new GetRRList(zoneName, name, type, qualifier); + + return nextOrNull(new ResourceRecordByNameAndTypeIterator(api, getRRList)); + } + + private List notIn(List a, List b) { + List r = new ArrayList(); + + for (E i : a) { + if (!b.contains(i)) { + r.add(i); + } + } + + return r; + } + + private List copy(List a) { + List r = new ArrayList(); + + r.addAll(a); + + return r; + } + + @Override + public void put(ResourceRecordSet rrset) { + checkArgument(rrset != null && !rrset.records().isEmpty(), "rrset was empty"); + + Integer ttlToApply = rrset.ttl() != null ? rrset.ttl() : 86400; + + ResourceRecordSet oldRRSet = null; + if (rrset.qualifier() != null) { + oldRRSet = getByNameTypeAndQualifier(rrset.name(), rrset.type(), rrset.qualifier()); + } else { + oldRRSet = nextOrNull(iterateByNameAndType(rrset.name(), rrset.type())); + } + + List> toAdd; + List> toDel; + if (oldRRSet != null) { + if (equal(oldRRSet.ttl(), ttlToApply)) { + toDel = notIn(oldRRSet.records(), rrset.records()); + toAdd = notIn(rrset.records(), oldRRSet.records()); + } else { + toDel = copy(oldRRSet.records()); + toAdd = copy(rrset.records()); + } + } else { + toDel = null; + toAdd = copy(rrset.records()); + } + + if (toAdd.isEmpty() && (toDel == null || toDel.isEmpty())) { + return; + } + + rrset = ResourceRecordSet.builder() + .name(rrset.name()) + .type(rrset.type()) + .ttl(ttlToApply) + .addAll(toAdd) + .build(); + + ResourceRecordSet> rrsetToBeDeleted = null; + if (toDel != null && !toDel.isEmpty()) { + rrsetToBeDeleted = ResourceRecordSet.builder() + .name(oldRRSet.name()) + .type(oldRRSet.type()) + .ttl(oldRRSet.ttl()) + .addAll(toDel) + .build(); + } + + api.updateResourceRecords(zoneName, rrset, rrsetToBeDeleted); + } + + @Override + public void deleteByNameAndType(String name, String type) { + checkNotNull(name, "name"); + checkNotNull(type, "type"); + + ResourceRecordSet rrSet = nextOrNull(iterateByNameAndType(name, type)); + if (rrSet != null) { + api.deleteResourceRecords(zoneName, rrSet); + } + } + + @Override + public void deleteByNameTypeAndQualifier(String name, String type, String qualifier) { + checkNotNull(name, "name"); + checkNotNull(type, "type"); + checkNotNull(qualifier, "rdata for the record"); + + ResourceRecordSet rrSet = getByNameTypeAndQualifier(name, type, qualifier); + if (rrSet != null) { + api.deleteResourceRecords(zoneName, rrSet); + } + } + + static final class Factory implements denominator.AllProfileResourceRecordSetApi.Factory { + + private final VerisignDns api; + + @Inject + Factory(VerisignDns api) { + this.api = api; + } + + @Override + public VerisignDnsAllProfileResourceRecordSetApi create(String name) { + return new VerisignDnsAllProfileResourceRecordSetApi(api, name); + } + } + +} diff --git a/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsContentHandlers.java b/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsContentHandlers.java new file mode 100644 index 00000000..12d96358 --- /dev/null +++ b/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsContentHandlers.java @@ -0,0 +1,225 @@ +package denominator.verisigndns; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import denominator.model.Zone; +import feign.sax.SAXDecoder.ContentHandlerWithResult; + +final class VerisignDnsContentHandlers { + + private VerisignDnsContentHandlers() { + } + + abstract static class ElementHandler extends DefaultHandler { + + private Deque elements = null; + private String parentEl; + + protected ElementHandler(String parentEl) { + this.parentEl = parentEl; + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) + throws SAXException { + + if (parentEl.equals(qName)) { + elements = new ArrayDeque(); + } + + if (elements != null) { + elements.push(qName); + } + } + + @Override + public void endElement(String uri, String localName, String qName) throws SAXException { + + if (elements != null) { + elements.pop(); + } + + if (parentEl.equals(qName)) { + elements = null; + } + } + + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + if (elements == null) { + return; + } + + processElValue(elements.peek(), ch, start, length); + } + + protected abstract void processElValue(String currentEl, char[] ch, int start, int length); + } + + static class ZoneHandler extends ElementHandler implements ContentHandlerWithResult { + private String domainName; + private String email; + private int ttl; + + ZoneHandler() { + super("ns3:getZoneInfoRes"); + } + + @Override + protected void processElValue(String currentEl, char[] ch, int start, int length) { + if ("ns3:domainName".equals(currentEl)) { + domainName = val(ch, start, length); + } else if ("ns3:email".equals(currentEl)) { + email = val(ch, start, length); + } else if ("ns3:ttl".equals(currentEl)) { + ttl = Integer.valueOf(val(ch, start, length)); + } + } + + @Override + public Zone result() { + Zone zone = null; + + if (domainName != null) { + zone = Zone.create(domainName, domainName, ttl, email); + } + + return zone; + } + } + + static class ZoneListHandler extends ElementHandler implements ContentHandlerWithResult> { + private int count = 0; + private List zones = new ArrayList(); + + ZoneListHandler() { + super("ns3:getZoneListRes"); + } + + @Override + protected void processElValue(String currentEl, char[] ch, int start, int length) { + + if ("ns3:totalCount".equals(currentEl)) { + String value = val(ch, start, length); + count = Integer.valueOf(value); + } else if ("ns3:domainName".equals(currentEl)) { + String value = val(ch, start, length); + zones.add(Zone.create(value, value, 86400, "mdnshelp@verisign.com")); + } + } + + @Override + public Page result() { + return new Page(zones, count); + } + } + + static class RRHandler extends ElementHandler implements ContentHandlerWithResult> { + private int count = 0; + private List rrList = new ArrayList(); + + RRHandler() { + super("ns3:getResourceRecordListRes"); + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) + throws SAXException { + super.startElement(uri, localName, qName, attributes); + if ("ns3:resourceRecord".equals(qName)) { + rrList.add(new ResourceRecord()); + } + } + + @Override + protected void processElValue(String currentEl, char[] ch, int start, int length) { + if (rrList.isEmpty()) { + return; + } + + ResourceRecord resourceRecord = rrList.get(rrList.size() - 1); + String value = val(ch, start, length); + if ("ns3:totalCount".equals(currentEl)) { + count = Integer.valueOf(value); + } else if ("ns3:resourceRecordId".equals(currentEl)) { + resourceRecord.id = value; + } else if ("ns3:owner".equals(currentEl)) { + resourceRecord.name = value; + } else if ("ns3:type".equals(currentEl)) { + resourceRecord.type = value; + } else if ("ns3:rData".equals(currentEl)) { + resourceRecord.rdata = value; + } else if ("ns3:ttl".equals(currentEl)) { + resourceRecord.ttl = Integer.valueOf(value); + } + } + + @Override + public Page result() { + return new Page(rrList, count); + } + } + + static String val(char[] ch, int start, int length) { + return new String(ch, start, length).trim(); + } + + static class Page { + private final List list; + private final int count; + + Page(List list, int count) { + this.list = list; + this.count = count; + } + + List getList() { + return list; + } + + int getCount() { + return count; + } + + @Override + public String toString() { + return String.format("page[count=%d total=%d]", list != null ? list.size() : 0, count); + } + } + + static class ResourceRecord { + private String id; + private String name; + private String type; + private String rdata; + private Integer ttl; + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public String getRdata() { + return rdata; + } + + public Integer getTtl() { + return ttl; + } + + @Override + public String toString() { + return String.format("rr[id=%s name=%s type=%s rdata=\"%s\" ttl=%d]", id, name, type, rdata, ttl); + } + } +} diff --git a/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsEncoder.java b/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsEncoder.java new file mode 100644 index 00000000..bdadfbc2 --- /dev/null +++ b/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsEncoder.java @@ -0,0 +1,368 @@ +package denominator.verisigndns; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import denominator.common.Util; +import denominator.model.ResourceRecordSet; +import denominator.model.Zone; +import feign.RequestTemplate; +import feign.codec.EncodeException; +import feign.codec.Encoder; + +final class VerisignDnsEncoder implements Encoder { + + private static final String NS_API_1 = "api1"; + private static final String NS_API_2 = "api2"; + + @SuppressWarnings("unchecked") + @Override + public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { + + Map params = Map.class.cast(object); + + Node node = null; + if (params.containsKey("rrSet")) { + node = encodeRRSet(params); + } else if (params.containsKey("createZone")) { + node = encodeCreateZone(params); + } else if (params.containsKey("updateSoa")) { + node = encodeUpdateSoa(params); + } else if (params.containsKey("getZone")) { + node = encodeGetZone(params); + } else if (params.containsKey("getZoneList")) { + node = encodeGetZoneList(params); + } else if (params.containsKey("deleteZone")) { + node = encodeDeleteZone(params); + } else if (params.containsKey("getRRList")) { + node = encodeGetRRList(params); + } else if (params.containsKey("deleteRRSet")) { + node = encodeDeleteRRSet(params); + } else { + throw new EncodeException("Unsupported param key"); + } + + template.body(node.toXml()); + } + + private String normalize(String email) { + email = email.replace("@", "."); + if (!email.endsWith(".")) { + email += "."; + } + return email; + } + + private Node encodeCreateZone(Map params) { + + Zone zone = Zone.class.cast(params.get("createZone")); + if (zone == null) { + return null; + } + + TagNode zoneNode = new TagNode(NS_API_1, "createZone"); + zoneNode.add(NS_API_1, "domainName", zone.name()); + zoneNode.add(NS_API_1, "type", "DNS Hosting"); + return zoneNode; + } + + private Node encodeUpdateSoa(Map params) { + + Zone zone = Zone.class.cast(params.get("updateSoa")); + if (zone == null) { + return null; + } + + TagNode soaNode = new TagNode(NS_API_1, "zoneSOAInfo"); + soaNode.add(NS_API_1, "email", normalize(zone.email())); + soaNode.add(NS_API_1, "retry", 7200); + soaNode.add(NS_API_1, "ttl", zone.ttl()); + soaNode.add(NS_API_1, "refresh", 86400); + soaNode.add(NS_API_1, "expire", 1209600); + + TagNode zoneNode = new TagNode(NS_API_1, "updateSOA"); + zoneNode.add(NS_API_1, "domainName", zone.name()); + zoneNode.add(soaNode); + return zoneNode; + } + + private Node encodeGetZone(Map params) { + + String zoneName = String.class.cast(params.get("getZone")); + if (zoneName == null) { + return null; + } + + TagNode zoneNode = new TagNode(NS_API_1, "getZoneInfo"); + zoneNode.add(NS_API_1, "domainName", zoneName); + return zoneNode; + } + + private Node encodeGetZoneList(Map params) { + + Paging paging = Paging.class.cast(params.get("getZoneList")); + if (paging == null) { + return null; + } + + TagNode zoneListNode = new TagNode(NS_API_1, "getZoneList"); + zoneListNode.add(toPagingNode(paging)); + return zoneListNode; + } + + private Node encodeDeleteZone(Map params) { + + String zoneName = (String) params.get("deleteZone"); + if (zoneName == null) { + return null; + } + + return new TagNode(NS_API_1, "deleteZone").add(NS_API_1, "domainName", zoneName); + } + + private Node encodeDeleteRRSet(Map params) { + + ResourceRecordSet oldRRSet = ResourceRecordSet.class.cast(params.get("deleteRRSet")); + if (oldRRSet == null) { + return null; + } + + String zoneName = String.class.cast(params.get("zone")); + + TagNode bulkUpdateZoneNode = new TagNode(NS_API_2, "bulkUpdateSingleZone"); + bulkUpdateZoneNode.add(NS_API_2, "domainName", zoneName); + bulkUpdateZoneNode.add(toRRNode(NS_API_2, "deleteResourceRecords", oldRRSet, false)); + return bulkUpdateZoneNode; + } + + private Node toPagingNode(Paging paging) { + TagNode pagingNode = null; + + if (paging != null) { + pagingNode = new TagNode(NS_API_1, "listPagingInfo"); + + pagingNode.add(NS_API_1, "pageNumber", paging.pageNumber); + pagingNode.add(NS_API_1, "pageSize", paging.pageSize); + } + + return pagingNode; + } + + private Node encodeGetRRList(Map params) { + + GetRRList getRRList = GetRRList.class.cast(params.get("getRRList")); + if (getRRList == null) { + return null; + } + + String zoneName = String.class.cast(params.get("zone")); + + TagNode getRRListNode = new TagNode(NS_API_1, "getResourceRecordList"); + getRRListNode.add(NS_API_1, "domainName", zoneName); + getRRListNode.add(NS_API_1, "owner", getRRList.ownerName); + getRRListNode.add(NS_API_1, "resourceRecordType", getRRList.type); + getRRListNode.add(toPagingNode(getRRList.paging)); + + return getRRListNode; + } + + private Node encodeRRSet(Map params) { + + ResourceRecordSet rrSet = ResourceRecordSet.class.cast(params.get("rrSet")); + if (rrSet == null) { + return null; + } + + String zoneName = String.class.cast(params.get("zone")); + ResourceRecordSet oldRRSet = ResourceRecordSet.class.cast(params.get("oldRRSet")); + + TagNode bulkUpdateZoneNode = new TagNode(NS_API_2, "bulkUpdateSingleZone"); + bulkUpdateZoneNode.add(NS_API_2, "domainName", zoneName); + if (!rrSet.records().isEmpty()) { + bulkUpdateZoneNode.add(toRRNode(NS_API_2, "createResourceRecords", rrSet, true)); + } + if (oldRRSet != null && !oldRRSet.records().isEmpty()) { + bulkUpdateZoneNode.add(toRRNode(NS_API_2, "deleteResourceRecords", oldRRSet, false)); + } + + return bulkUpdateZoneNode; + } + + private Node toRRNode(String ns, String tag, ResourceRecordSet rrSet, boolean includeTtl) { + + String name = rrSet.name(); + String type = rrSet.type(); + Integer ttl = rrSet.ttl(); + TagNode rrsNode = new TagNode(ns, tag); + + for (Map record : rrSet.records()) { + TagNode rrNode = new TagNode(ns, "resourceRecord"); + rrNode.add(ns, "owner", name); + rrNode.add(ns, "type", type); + rrNode.add(ns, "rData", Util.flatten(record)); + if (includeTtl && ttl != null) { + rrNode.add(ns, "ttl", ttl.intValue()); + } + rrsNode.add(rrNode); + } + return rrsNode; + } + + static class GetRRList { + private String zoneName; + private String ownerName; + private String type; + private String viewName; + private Paging paging; + + public GetRRList(String zoneName) { + this.zoneName = zoneName; + } + + public GetRRList(String zoneName, String ownerName) { + this.zoneName = zoneName; + this.ownerName = ownerName; + } + + public GetRRList(String zoneName, String ownerName, String type) { + this.zoneName = zoneName; + this.ownerName = ownerName; + this.type = type; + } + + public GetRRList(String zoneName, String ownerName, String type, String viewName) { + this.zoneName = zoneName; + this.ownerName = ownerName; + this.type = type; + this.viewName = viewName; + } + + public String getZoneName() { + return zoneName; + } + + public boolean nextPage() { + if (paging == null) { + paging = new Paging(1); + return true; + } else { + return paging.nextPage(); + } + } + + public void setTotal(int total) { + paging.setTotal(total); + } + + @Override + public String toString() { + return String.format("getrrlist[zone=%s owner=%s type=%s view=%s paging=%s]", + zoneName, ownerName, type, viewName, paging); + } + } + + static class Paging { + private int pageNumber; + private int pageSize; + private int total; + + Paging(int pageNumber) { + this.pageNumber = pageNumber; + this.pageSize = 100; + } + + Paging(int pageNumber, int pageSize) { + this.pageNumber = pageNumber; + this.pageSize = pageSize; + } + + void setTotal(int total) { + this.total = total; + } + + int getPages() { + return (total / pageSize) + ((total % pageSize) == 0 ? 0 : 1); + } + + boolean nextPage() { + return ++pageNumber < total; + } + + @Override + public String toString() { + return String.format("paging[page=%d size=%d total=%d pages=%d]", pageNumber, pageSize, total, getPages()); + } + } + + interface Node { + String toXml(); + } + + class TextNode implements Node { + + private final String value; + + TextNode(String value) { + this.value = value; + } + + @Override + public String toXml() { + return value; + } + } + + class TagNode implements Node { + + private final String ns; + private final String tag; + private final List children; + + TagNode(String ns, String tag) { + this.ns = ns; + this.tag = tag; + this.children = new ArrayList(); + } + + String getTag() { + return tag; + } + + List getChildren() { + return children; + } + + TagNode add(Node node) { + if (node != null) { + children.add(node); + } + return this; + } + + TagNode add(String cns, String ctag, String value) { + if (value != null) { + children.add(new TagNode(cns, ctag).add(new TextNode(value))); + } + return this; + } + + TagNode add(String cns, String ctag, int value) { + return add(cns, ctag, Integer.toString(value)); + } + + @Override + public String toXml() { + + StringBuilder sb = new StringBuilder(); + sb.append("<").append(ns).append(":").append(tag).append(">"); + for (Node child : children) { + sb.append(child.toXml()); + } + sb.append(""); + + return sb.toString(); + } + } +} diff --git a/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsErrorDecoder.java b/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsErrorDecoder.java new file mode 100644 index 00000000..06fd269d --- /dev/null +++ b/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsErrorDecoder.java @@ -0,0 +1,85 @@ +package denominator.verisigndns; + +import java.io.IOException; + +import javax.inject.Inject; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import feign.FeignException; +import feign.Response; +import feign.codec.Decoder; +import feign.codec.ErrorDecoder; +import feign.sax.SAXDecoder.ContentHandlerWithResult; + +final class VerisignDnsErrorDecoder implements ErrorDecoder { + + private final Decoder decoder; + + VerisignDnsErrorDecoder(Decoder decoder) { + this.decoder = decoder; + } + + @Override + public Exception decode(String methodKey, Response response) { + + try { + Object errorObject = decoder.decode(response, VerisignDnsError.class); + VerisignDnsError error = VerisignDnsError.class.cast(errorObject); + if (error == null) { + return FeignException.errorStatus(methodKey, response); + } + + StringBuilder message = new StringBuilder(); + message.append(methodKey).append(" failed"); + if (error.code != null) { + message.append(" with error ").append(error.code); + } + if (error.description != null) { + message.append(": ").append(error.description); + } + + int errorCode = -1; + if (error.description.equalsIgnoreCase("The domain name could not be found.")) { + errorCode = VerisignDnsException.DOMAIN_NOT_FOUND; + } else if (error.description + .equalsIgnoreCase("Domain already exists. Please verify your domain name.")) { + errorCode = VerisignDnsException.DOMAIN_ALREADY_EXISTS; + } + + return new VerisignDnsException(message.toString(), errorCode, error.description); + } catch (IOException ignored) { + return FeignException.errorStatus(methodKey, response); + } catch (RuntimeException propagate) { + return propagate; + } + } + + static class VerisignDnsError extends DefaultHandler implements + ContentHandlerWithResult { + + private String description; + private String code; + + @Inject + VerisignDnsError() { + } + + @Override + public VerisignDnsError result() { + return (code == null && description == null) ? null : this; + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) + throws SAXException { + + if ("ns3:reason".equals(qName)) { + description = attributes.getValue("description"); + code = attributes.getValue("code"); + } + } + } +} diff --git a/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsException.java b/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsException.java new file mode 100644 index 00000000..076def96 --- /dev/null +++ b/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsException.java @@ -0,0 +1,28 @@ +package denominator.verisigndns; + +import feign.FeignException; + +final class VerisignDnsException extends FeignException { + + static final int SYSTEM_ERROR = -1; + static final int DOMAIN_NOT_FOUND = 1; + static final int DOMAIN_ALREADY_EXISTS = 2; + + private static final long serialVersionUID = 1L; + private final int code; + private final String description; + + VerisignDnsException(String message, int code, String description) { + super(message); + this.code = code; + this.description = description; + } + + public int code() { + return code; + } + + public String description() { + return description; + } +} diff --git a/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsProvider.java b/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsProvider.java new file mode 100644 index 00000000..0354e0fe --- /dev/null +++ b/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsProvider.java @@ -0,0 +1,146 @@ +package denominator.verisigndns; + +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import javax.inject.Singleton; + +import dagger.Provides; +import denominator.AllProfileResourceRecordSetApi; +import denominator.BasicProvider; +import denominator.CheckConnection; +import denominator.DNSApiManager; +import denominator.ResourceRecordSetApi; +import denominator.ZoneApi; +import denominator.config.GeoUnsupported; +import denominator.config.NothingToClose; +import denominator.config.WeightedUnsupported; +import denominator.verisigndns.VerisignDnsContentHandlers.RRHandler; +import denominator.verisigndns.VerisignDnsContentHandlers.ZoneHandler; +import denominator.verisigndns.VerisignDnsContentHandlers.ZoneListHandler; +import denominator.verisigndns.VerisignDnsErrorDecoder.VerisignDnsError; +import feign.Feign; +import feign.Logger; +import feign.Request.Options; +import feign.codec.Decoder; +import feign.sax.SAXDecoder; + +public class VerisignDnsProvider extends BasicProvider { + private static final String DEFAULT_URL = "https://api.verisigndns.com/dnsa-ws/V2.0/dnsaapi?wsdl"; + private final String url; + + public VerisignDnsProvider() { + this(null); + } + + /** + * Construct a new provider for the Verisign Dns service. + * + * @param url if empty or null use default + */ + public VerisignDnsProvider(String url) { + this.url = url == null || url.isEmpty() ? DEFAULT_URL : url; + } + + @Override + public String url() { + return url; + } + + @Override + public Set basicRecordTypes() { + Set types = new LinkedHashSet(); + types.addAll(Arrays.asList("A", "AAAA", "CNAME", "MX", "NAPTR", "NS", "PTR", "SRV", "TXT")); + return types; + } + + @Override + public Map> credentialTypeToParameterNames() { + Map> options = new LinkedHashMap>(); + options.put("password", Arrays.asList("username", "password")); + return options; + } + + @dagger.Module(injects = { DNSApiManager.class }, + complete = false, + includes = { NothingToClose.class, WeightedUnsupported.class, GeoUnsupported.class, FeignModule.class }) + public static final class Module { + + @Provides + CheckConnection checkConnection(HostedZonesReadable checkConnection) { + return checkConnection; + } + + @Provides + @Singleton + ZoneApi provideZoneApi(VerisignDnsZoneApi api) { + return api; + } + + + @Provides + @Singleton + ResourceRecordSetApi.Factory provideResourceRecordSetApiFactory( + VerisignDnsResourceRecordSetApi.Factory factory) { + return factory; + } + + @Provides + @Singleton + AllProfileResourceRecordSetApi.Factory provideAllProfileResourceRecordSetApiFactory( + VerisignDnsAllProfileResourceRecordSetApi.Factory factory) { + return factory; + } + + } + + @dagger.Module(injects = VerisignDnsResourceRecordSetApi.Factory.class, complete = false) + public static final class FeignModule { + + @Provides + @Singleton + VerisignDns verisignDns(Feign feign, VerisignDnsTarget target) { + return feign.newInstance(target); + } + + @Provides + Logger logger() { + return new Logger.NoOpLogger(); + } + + @Provides + Logger.Level logLevel() { + return Logger.Level.NONE; + } + + @Provides + @Singleton + Feign feign(Logger logger, Logger.Level logLevel) { + + Options options = new Options(10 * 1000, 10 * 60 * 1000); + Decoder decoder = decoder(); + + return Feign.builder() + .logger(logger) + .logLevel(logLevel) + .options(options) + .encoder(new VerisignDnsEncoder()) + .decoder(decoder) + .errorDecoder(new VerisignDnsErrorDecoder(decoder)) + .build(); + } + + static Decoder decoder() { + return SAXDecoder.builder() + .registerContentHandler(RRHandler.class) + .registerContentHandler(ZoneHandler.class) + .registerContentHandler(ZoneListHandler.class) + .registerContentHandler(VerisignDnsError.class) + .build(); + } + } +} diff --git a/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsResourceRecordSetApi.java b/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsResourceRecordSetApi.java new file mode 100644 index 00000000..30ad8ee0 --- /dev/null +++ b/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsResourceRecordSetApi.java @@ -0,0 +1,60 @@ +package denominator.verisigndns; + +import static denominator.common.Util.nextOrNull; + +import java.util.Iterator; + +import javax.inject.Inject; + +import denominator.ResourceRecordSetApi; +import denominator.model.ResourceRecordSet; + +final class VerisignDnsResourceRecordSetApi implements ResourceRecordSetApi { + + private final VerisignDnsAllProfileResourceRecordSetApi allApi; + + public VerisignDnsResourceRecordSetApi(VerisignDnsAllProfileResourceRecordSetApi allApi) { + this.allApi = allApi; + } + + @Override + public Iterator> iterator() { + return allApi.iterator(); + } + + @Override + public Iterator> iterateByName(String name) { + return allApi.iterateByName(name); + } + + @Override + public ResourceRecordSet getByNameAndType(String name, String type) { + return nextOrNull(allApi.iterateByNameAndType(name, type)); + } + + @Override + public void put(ResourceRecordSet rrset) { + allApi.put(rrset); + } + + @Override + public void deleteByNameAndType(String name, String type) { + allApi.deleteByNameAndType(name, type); + } + + static final class Factory implements ResourceRecordSetApi.Factory { + + private final VerisignDnsAllProfileResourceRecordSetApi.Factory allApi; + + @Inject + Factory(VerisignDnsAllProfileResourceRecordSetApi.Factory allApi) { + this.allApi = allApi; + } + + @Override + public ResourceRecordSetApi create(String name) { + return new VerisignDnsResourceRecordSetApi(allApi.create(name)); + } + } + +} diff --git a/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsTarget.java b/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsTarget.java new file mode 100644 index 00000000..87536c0f --- /dev/null +++ b/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsTarget.java @@ -0,0 +1,102 @@ +package denominator.verisigndns; + +import static denominator.common.Preconditions.checkNotNull; +import static feign.Util.UTF_8; +import static java.lang.String.format; + +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.inject.Inject; + +import denominator.Credentials; +import denominator.Provider; +import feign.Request; +import feign.RequestTemplate; +import feign.Target; + +final class VerisignDnsTarget implements Target { + static final AtomicInteger MSGID = new AtomicInteger(); + // formatter:off + static final String SOAP_TEMPLATE = + "\n" + + " \n" + + " \n" + + " %d\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " %s\n" + + " %s\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " %s\n" + + " \n" + + ""; + // formatter:on + + private final Provider provider; + private final javax.inject.Provider credentials; + + @Inject + VerisignDnsTarget(Provider provider, javax.inject.Provider credentials) { + this.provider = provider; + this.credentials = credentials; + } + + @Override + public Class type() { + return VerisignDns.class; + } + + @Override + public String name() { + return provider.name(); + } + + @Override + public String url() { + return provider.url(); + } + + @Override + public Request apply(RequestTemplate in) { + String username; + String password; + + Credentials creds = credentials.get(); + if (creds instanceof List) { + @SuppressWarnings("unchecked") + List listCreds = (List) creds; + username = listCreds.get(0).toString(); + password = listCreds.get(1).toString(); + } else if (creds instanceof Map) { + @SuppressWarnings("unchecked") + Map mapCreds = (Map) creds; + username = checkNotNull(mapCreds.get("username"), "username").toString(); + password = checkNotNull(mapCreds.get("password"), "password").toString(); + } else { + throw new IllegalArgumentException("Unsupported credential type: " + creds); + } + + in.insert(0, url()); + in.body(xml(username, password, new String(in.body(), UTF_8))); + in.header("Host", URI.create(in.url()).getHost()); + in.header("Content-Type", "application/soap+xml"); + return in.request(); + } + + private String xml(String username, String password, String body) { + return format(SOAP_TEMPLATE, MSGID.getAndIncrement(), username, password, body); + } +} diff --git a/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsZoneApi.java b/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsZoneApi.java new file mode 100644 index 00000000..e2cadcb8 --- /dev/null +++ b/verisigndns/src/main/java/denominator/verisigndns/VerisignDnsZoneApi.java @@ -0,0 +1,101 @@ +package denominator.verisigndns; + +import static denominator.common.Util.singletonIterator; + +import java.util.Iterator; + +import javax.inject.Inject; + +import denominator.model.Zone; +import denominator.verisigndns.VerisignDnsContentHandlers.Page; +import denominator.verisigndns.VerisignDnsEncoder.Paging; + +final class VerisignDnsZoneApi implements denominator.ZoneApi { + + private final VerisignDns api; + + @Inject + VerisignDnsZoneApi(VerisignDns api) { + this.api = api; + } + + @Override + public Iterator iterator() { + return new Iterator() { + private Paging paging; + private Iterator current; + + private void check() { + boolean nextPage = false; + + if (paging == null) { + paging = new Paging(1); + nextPage = true; + } else if (!current.hasNext() && paging.nextPage()) { + nextPage = true; + } + + if (nextPage) { + Page page = api.getZones(paging); + paging.setTotal(page.getCount()); + current = page.getList().iterator(); + } + } + + @Override + public boolean hasNext() { + check(); + return current.hasNext(); + } + + @Override + public Zone next() { + check(); + return api.getZone(current.next().name()); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public Iterator iterateByName(String name) { + Zone zone = null; + try { + zone = api.getZone(name); + } catch (VerisignDnsException e) { + if (e.code() != VerisignDnsException.DOMAIN_NOT_FOUND) { + throw e; + } + } + return singletonIterator(zone); + } + + @Override + public String put(Zone zone) { + try { + api.createZone(zone); + } catch (VerisignDnsException e) { + if (e.code() != VerisignDnsException.DOMAIN_ALREADY_EXISTS) { + throw e; + } + } + + api.updateSoa(zone); + return zone.name(); + } + + @Override + public void delete(String zone) { + try { + api.deleteZone(zone); + } catch (VerisignDnsException e) { + if (e.code() != VerisignDnsException.DOMAIN_NOT_FOUND) { + throw e; + } + } + } +} diff --git a/verisigndns/src/main/resources/META-INF/services/denominator.Provider b/verisigndns/src/main/resources/META-INF/services/denominator.Provider new file mode 100644 index 00000000..dd32b01d --- /dev/null +++ b/verisigndns/src/main/resources/META-INF/services/denominator.Provider @@ -0,0 +1 @@ +denominator.verisigndns.VerisignDnsProvider diff --git a/verisigndns/src/test/java/denominator/verisigndns/HostedZonesReadableMockTest.java b/verisigndns/src/test/java/denominator/verisigndns/HostedZonesReadableMockTest.java new file mode 100644 index 00000000..33f7a9e2 --- /dev/null +++ b/verisigndns/src/test/java/denominator/verisigndns/HostedZonesReadableMockTest.java @@ -0,0 +1,49 @@ +package denominator.verisigndns; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Rule; +import org.junit.Test; + +import com.squareup.okhttp.mockwebserver.MockResponse; + +import denominator.DNSApiManager; + +public class HostedZonesReadableMockTest { + + @Rule + public final MockVerisignDnsServer server = new MockVerisignDnsServer(); + + @Test + public void singleRequestOnSuccess() throws Exception { + server + .enqueue(new MockResponse() + .setBody("" + + " true" + + " 1" + + " " + + " denominator.io" + + " DNS Hosting" + + " ACTIVE" + + " 2015-09-29T01:55:39.000Z" + + " 2015-09-30T00:25:53.000Z" + + " No" + + " " + "")); + + DNSApiManager api = server.connect(); + assertTrue(api.checkConnection()); + + server.assertRequest(); + } + + @Test + public void singleRequestOnFailure() throws Exception { + server.enqueue(new MockResponse().setResponseCode(500)); + + DNSApiManager api = server.connect(); + assertFalse(api.checkConnection()); + + server.assertRequest(); + } +} diff --git a/verisigndns/src/test/java/denominator/verisigndns/MockVerisignDnsServer.java b/verisigndns/src/test/java/denominator/verisigndns/MockVerisignDnsServer.java new file mode 100644 index 00000000..d4134c95 --- /dev/null +++ b/verisigndns/src/test/java/denominator/verisigndns/MockVerisignDnsServer.java @@ -0,0 +1,102 @@ +package denominator.verisigndns; + +import static denominator.assertj.MockWebServerAssertions.assertThat; +import static denominator.verisigndns.VerisignDnsTarget.SOAP_TEMPLATE; +import static java.lang.String.format; + +import java.io.IOException; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import com.squareup.okhttp.mockwebserver.MockResponse; +import com.squareup.okhttp.mockwebserver.MockWebServer; + +import denominator.Credentials; +import denominator.CredentialsConfiguration; +import denominator.DNSApiManager; +import denominator.Denominator; +import denominator.Credentials.ListCredentials; +import denominator.assertj.RecordedRequestAssert; + +public class MockVerisignDnsServer extends VerisignDnsProvider implements TestRule { + + private final MockWebServer delegate = new MockWebServer(); + private String username; + private String password; + private String soapTemplate; + + MockVerisignDnsServer() { + credentials("testuser", "password"); + } + + @Override + public String url() { + return "http://localhost:" + delegate.getPort(); + } + + DNSApiManager connect() { + return Denominator.create(this, CredentialsConfiguration.credentials(credentials())); + } + + Credentials credentials() { + return ListCredentials.from(username, password); + } + + MockVerisignDnsServer credentials(String username, String password) { + this.username = username; + this.password = password; + this.soapTemplate = format(SOAP_TEMPLATE, System.currentTimeMillis(), username, password, "%s"); + return this; + } + + void enqueue(MockResponse mockResponse) { + delegate.enqueue(mockResponse); + } + + void enqueueError(String code, String description) { + delegate.enqueue(new MockResponse().setResponseCode(500).setBody( + format(FAULT_TEMPLATE, description, code, description))); + } + + RecordedRequestAssert assertRequest() throws InterruptedException { + return assertThat(delegate.takeRequest()); + } + + RecordedRequestAssert assertSoapBody(String soapBody) throws InterruptedException { + return assertThat(delegate.takeRequest()).hasMethod("POST").hasPath("/") + .hasBody(format(soapTemplate, soapBody)); + } + + void shutdown() throws IOException { + delegate.shutdown(); + } + + @Override + public Statement apply(Statement base, Description description) { + return delegate.apply(base, description); + } + + @dagger.Module(injects = DNSApiManager.class, complete = false, + includes = VerisignDnsProvider.Module.class) + static final class Module { + + } + + static final String FAULT_TEMPLATE = + "" + + " " + + " ns3:Receiver" + + " " + + " " + + " %s" + + " " + + " " + + " " + + " false" + + " " + + " " + + " " + + ""; +} diff --git a/verisigndns/src/test/java/denominator/verisigndns/VerisignDnsProviderDynamicUpdateMockTest.java b/verisigndns/src/test/java/denominator/verisigndns/VerisignDnsProviderDynamicUpdateMockTest.java new file mode 100644 index 00000000..db6ed781 --- /dev/null +++ b/verisigndns/src/test/java/denominator/verisigndns/VerisignDnsProviderDynamicUpdateMockTest.java @@ -0,0 +1,84 @@ +package denominator.verisigndns; + +import static denominator.CredentialsConfiguration.credentials; + +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Rule; +import org.junit.Test; + +import com.squareup.okhttp.mockwebserver.MockResponse; + +import dagger.Module; +import dagger.Provides; +import denominator.Credentials; +import denominator.Credentials.ListCredentials; +import denominator.DNSApi; +import denominator.Denominator; + +public class VerisignDnsProviderDynamicUpdateMockTest { + + @Rule + public MockVerisignDnsServer server = new MockVerisignDnsServer(); + + @Test + public void dynamicEndpointUpdates() throws Exception { + final AtomicReference url = new AtomicReference(server.url()); + server.enqueue(new MockResponse().setBody("")); + + DNSApi api = Denominator.create(new VerisignDnsProvider() { + @Override + public String url() { + return url.get(); + } + }, credentials(server.credentials())).api(); + + api.zones().iterator().hasNext(); + server.assertRequest(); + + MockVerisignDnsServer server2 = new MockVerisignDnsServer(); + url.set(server2.url()); + server2.enqueue(new MockResponse().setBody("")); + + api.zones().iterator().hasNext(); + server2.assertRequest(); + server2.shutdown(); + } + + @Test + public void dynamicCredentialUpdates() throws Exception { + server.enqueue(new MockResponse() + .setBody("")); + + AtomicReference dynamicCredentials = + new AtomicReference(server.credentials()); + + DNSApi api = Denominator.create(server, new OverrideCredentials(dynamicCredentials)).api(); + + api.zones().iterator().hasNext(); + server.assertRequest(); + + dynamicCredentials.set(ListCredentials.from("bob", "comeon")); + server.credentials("bob", "comeon"); + server.enqueue(new MockResponse() + .setBody("")); + + api.zones().iterator().hasNext(); + server.assertRequest(); + } + + @Module(complete = false, library = true, overrides = true) + static class OverrideCredentials { + + final AtomicReference dynamicCredentials; + + OverrideCredentials(AtomicReference dynamicCredentials) { + this.dynamicCredentials = dynamicCredentials; + } + + @Provides + public Credentials get() { + return dynamicCredentials.get(); + } + } +} diff --git a/verisigndns/src/test/java/denominator/verisigndns/VerisignDnsProviderTest.java b/verisigndns/src/test/java/denominator/verisigndns/VerisignDnsProviderTest.java new file mode 100644 index 00000000..dbf38534 --- /dev/null +++ b/verisigndns/src/test/java/denominator/verisigndns/VerisignDnsProviderTest.java @@ -0,0 +1,79 @@ +package denominator.verisigndns; + +import static denominator.CredentialsConfiguration.credentials; +import static denominator.Denominator.create; +import static denominator.Providers.list; +import static denominator.Providers.provide; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import dagger.ObjectGraph; +import denominator.Credentials.MapCredentials; +import denominator.DNSApiManager; +import denominator.Provider; + +public class VerisignDnsProviderTest { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private static final Provider PROVIDER = new VerisignDnsProvider(); + + @Test + public void testVerisignDnsMetadata() { + assertThat(PROVIDER.name()).isEqualTo("verisigndns"); + assertThat(PROVIDER.supportsDuplicateZoneNames()).isFalse(); + assertThat(PROVIDER.credentialTypeToParameterNames()).containsEntry("password", + Arrays.asList("username", "password")); + } + + @Test + public void testVerisignDnsRegistered() { + assertThat(list()).contains(PROVIDER); + } + + @Test + public void testProviderWiresVerisignDnsZoneApi() { + DNSApiManager manager = create(PROVIDER, credentials("username", "password")); + assertThat(manager.api().zones()).isInstanceOf(VerisignDnsZoneApi.class); + manager = create("verisigndns", credentials("username", "password")); + assertThat(manager.api().zones()).isInstanceOf(VerisignDnsZoneApi.class); + + Map map = new LinkedHashMap(); + map.put("username", "U"); + map.put("password", "P"); + manager = create("verisigndns", credentials(MapCredentials.from(map))); + assertThat(manager.api().zones()).isInstanceOf(VerisignDnsZoneApi.class); + } + + @Test + public void testCredentialsRequired() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("no credentials supplied. " + PROVIDER.name() + " requires username,password"); + + create(PROVIDER).api().zones().iterator().hasNext(); + } + + @Test + public void testTwoPartCredentialsRequired() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("incorrect credentials supplied. " + PROVIDER.name() + " requires username,password"); + + create(PROVIDER, credentials("customer", "username", "password")).api().zones().iterator().hasNext(); + } + + @Test + public void testViaDagger() { + DNSApiManager manager = + ObjectGraph.create(provide(new VerisignDnsProvider()), new VerisignDnsProvider.Module(), + credentials("username", "password")).get(DNSApiManager.class); + assertThat(manager.api().zones()).isInstanceOf(VerisignDnsZoneApi.class); + } +} diff --git a/verisigndns/src/test/java/denominator/verisigndns/VerisignDnsReadOnlyLiveTest.java b/verisigndns/src/test/java/denominator/verisigndns/VerisignDnsReadOnlyLiveTest.java new file mode 100644 index 00000000..6cd1d2bf --- /dev/null +++ b/verisigndns/src/test/java/denominator/verisigndns/VerisignDnsReadOnlyLiveTest.java @@ -0,0 +1,9 @@ +package denominator.verisigndns; + +import denominator.ReadOnlyLiveTest; +import denominator.Live.UseTestGraph; + +@UseTestGraph(VerisignDnsTestGraph.class) +public class VerisignDnsReadOnlyLiveTest extends ReadOnlyLiveTest { + +} diff --git a/verisigndns/src/test/java/denominator/verisigndns/VerisignDnsResourceRecordSetApiMockTest.java b/verisigndns/src/test/java/denominator/verisigndns/VerisignDnsResourceRecordSetApiMockTest.java new file mode 100644 index 00000000..35d510b0 --- /dev/null +++ b/verisigndns/src/test/java/denominator/verisigndns/VerisignDnsResourceRecordSetApiMockTest.java @@ -0,0 +1,243 @@ +package denominator.verisigndns; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Rule; +import org.junit.Test; + +import com.squareup.okhttp.mockwebserver.MockResponse; + +import denominator.AllProfileResourceRecordSetApi; +import denominator.common.Util; +import denominator.model.ResourceRecordSet; + +public class VerisignDnsResourceRecordSetApiMockTest { + + @Rule + public final MockVerisignDnsServer server = new MockVerisignDnsServer(); + + @Test + public void iteratorWhenPresent() throws Exception { + server + .enqueue(new MockResponse() + .setBody("" + + " true" + + " 1" + + " " + + " 3194811" + + " www.denominator.io." + + " A" + + " 86400" + + " 127.0.0.1" + + " " + + "")); + AllProfileResourceRecordSetApi recordSetsInZoneApi = + server.connect().api().recordSetsInZone("denominator.io"); + + assertThat(recordSetsInZoneApi.iterator()).containsExactly( + ResourceRecordSet.builder().name("www").type("A").ttl(86400) + .add(Util.toMap("A", "127.0.0.1")).build()); + } + + @Test + public void iteratorWhenAbsent() throws Exception { + server.enqueue(new MockResponse() + .setBody("")); + + AllProfileResourceRecordSetApi recordSetsInZoneApi = + server.connect().api().recordSetsInZone("denominator.io"); + assertThat(recordSetsInZoneApi.iterator()).isEmpty(); + } + + @Test + public void iterateByNameWhenPresent() throws Exception { + server + .enqueue(new MockResponse() + .setBody("" + + " true" + + " 1" + + " " + + " 3194811" + + " www.denominator.io." + + " A" + + " 86400" + + " 127.0.0.1" + + " " + + "")); + + AllProfileResourceRecordSetApi recordSetsInZoneApi = + server.connect().api().recordSetsInZone("denominator.io"); + + assertThat(recordSetsInZoneApi.iterateByName("www")).containsExactly( + ResourceRecordSet.builder().name("www").type("A").ttl(86400) + .add(Util.toMap("A", "127.0.0.1")).build()); + } + + @Test + public void iterateByNameWhenAbsent() throws Exception { + server.enqueue(new MockResponse() + .setBody("")); + + AllProfileResourceRecordSetApi recordSetsInZoneApi = + server.connect().api().recordSetsInZone("denominator.io"); + assertThat(recordSetsInZoneApi.iterator()).isEmpty(); + } + + @Test + public void putFirstRecordCreatesNewRRSet() throws Exception { + server + .enqueue(new MockResponse() + .setBody("" + + " true" + + " 0" + "")); + server + .enqueue(new MockResponse() + .setBody("" + + " true" + + " 1" + + " " + + " 3194811" + + " www.denominator.io." + + " A" + + " 86400" + + " 127.0.0.1" + + " " + + "")); + + AllProfileResourceRecordSetApi recordSetsInZoneApi = + server.connect().api().recordSetsInZone("denominator.io"); + assertThat(recordSetsInZoneApi.iterator()).isEmpty(); + + recordSetsInZoneApi.put(ResourceRecordSet.builder().name("www").type("A").ttl(86400) + .add(Util.toMap("A", "127.0.0.1")).build()); + } + + @Test + public void putSameRecordNoOp() throws Exception { + server + .enqueue(new MockResponse() + .setBody("" + + " true" + + " 1" + + " " + + " 3194811" + + " www.denominator.io." + + " A" + + " 86400" + + " 127.0.0.1" + + " " + + "")); + server + .enqueue(new MockResponse() + .setBody("" + + " true" + + " 1" + + " " + + " 3194811" + + " www.denominator.io." + + " A" + + " 86400" + + " 127.0.0.1" + + " " + + "")); + + AllProfileResourceRecordSetApi recordSetsInZoneApi = + server.connect().api().recordSetsInZone("denominator.io"); + + recordSetsInZoneApi.put(ResourceRecordSet.builder().name("www").type("A").ttl(86400) + .add(Util.toMap("A", "127.0.0.1")).build()); + + assertThat(recordSetsInZoneApi.iterator()).hasSize(1); + } + + @Test + public void putOneRecordReplacesRRSet() throws Exception { + server + .enqueue(new MockResponse() + .setBody("" + + " true" + + " 2" + + " " + + " 3194802" + + " www.denominator.io." + + " A" + + " 86400" + + " 127.0.0.11" + + " " + + " " + + " 3194811" + + " www1.denominator.io." + + " A" + + " 86400" + + " 127.0.0.12" + + " " + + "")); + server + .enqueue(new MockResponse() + .setBody("" + + " true" + + " 1" + + " " + + " 3194811" + + " www.denominator.io." + + " A" + + " 86400" + + " 127.0.0.1" + + " " + + "")); + + AllProfileResourceRecordSetApi recordSetsInZoneApi = + server.connect().api().recordSetsInZone("denominator.io"); + assertThat(recordSetsInZoneApi.iterator()).hasSize(2); + + recordSetsInZoneApi.put(ResourceRecordSet.builder().name("www").type("A").ttl(86400) + .add(Util.toMap("A", "127.0.0.1")).build()); + } + + @Test + public void deleteWhenPresent() throws Exception { + server + .enqueue(new MockResponse() + .setBody("" + + " true" + + " 1" + + " " + + " 3194811" + + " www.denominator.io." + + " A" + + " 86400" + + " 127.0.0.1" + + " " + + "")); + server + .enqueue(new MockResponse() + .setBody("" + + " true" + "")); + + AllProfileResourceRecordSetApi recordSetsInZoneApi = + server.connect().api().recordSetsInZone("denominator.io"); + recordSetsInZoneApi.deleteByNameAndType("www.denominator.io.", "A"); + } + + @Test + public void deleteWhenAbsent() throws Exception { + server + .enqueue(new MockResponse() + .setBody("" + + " true" + + " 1" + + " " + + " 3194811" + + " www.denominator.io." + + " A" + + " 86400" + + " 127.0.0.1" + + " " + + "")); + server.enqueue(new MockResponse()); + + AllProfileResourceRecordSetApi recordSetsInZoneApi = + server.connect().api().recordSetsInZone("denominator.io"); + recordSetsInZoneApi.deleteByNameAndType("www", "A"); + } +} diff --git a/verisigndns/src/test/java/denominator/verisigndns/VerisignDnsTestGraph.java b/verisigndns/src/test/java/denominator/verisigndns/VerisignDnsTestGraph.java new file mode 100644 index 00000000..1f4bb602 --- /dev/null +++ b/verisigndns/src/test/java/denominator/verisigndns/VerisignDnsTestGraph.java @@ -0,0 +1,15 @@ +package denominator.verisigndns; + +import static feign.Util.emptyToNull; +import static java.lang.System.getProperty; +import denominator.DNSApiManagerFactory; + +public class VerisignDnsTestGraph extends denominator.TestGraph { + + private static final String url = emptyToNull(getProperty("verisigndns.url")); + private static final String zone = emptyToNull(getProperty("verisigndns.zone")); + + public VerisignDnsTestGraph() { + super(DNSApiManagerFactory.create(new VerisignDnsProvider(url)), zone); + } +} diff --git a/verisigndns/src/test/java/denominator/verisigndns/VerisignDnsWriteCommandsLiveTest.java b/verisigndns/src/test/java/denominator/verisigndns/VerisignDnsWriteCommandsLiveTest.java new file mode 100644 index 00000000..6ecd4a9b --- /dev/null +++ b/verisigndns/src/test/java/denominator/verisigndns/VerisignDnsWriteCommandsLiveTest.java @@ -0,0 +1,9 @@ +package denominator.verisigndns; + +import denominator.WriteCommandsLiveTest; +import denominator.Live.UseTestGraph; + +@UseTestGraph(VerisignDnsTestGraph.class) +public class VerisignDnsWriteCommandsLiveTest extends WriteCommandsLiveTest { + +} diff --git a/verisigndns/src/test/java/denominator/verisigndns/VerisignDnsZoneApiMockTest.java b/verisigndns/src/test/java/denominator/verisigndns/VerisignDnsZoneApiMockTest.java new file mode 100644 index 00000000..32d25b19 --- /dev/null +++ b/verisigndns/src/test/java/denominator/verisigndns/VerisignDnsZoneApiMockTest.java @@ -0,0 +1,194 @@ +package denominator.verisigndns; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Rule; +import org.junit.Test; + +import com.squareup.okhttp.mockwebserver.MockResponse; + +import denominator.ZoneApi; +import denominator.model.Zone; + +public class VerisignDnsZoneApiMockTest { + + @Rule + public final MockVerisignDnsServer server = new MockVerisignDnsServer(); + + @Test + public void iteratorWhenPresent() throws Exception { + server + .enqueue(new MockResponse() + .setBody("" + + " true" + + " 1" + + " " + + " denominator.io" + + " DNS Hosting" + + " ACTIVE" + + " 2015-09-29T01:55:39.000Z" + + " 2015-09-30T00:25:53.000Z" + + " No" + + " " + "")); + + server + .enqueue(new MockResponse() + .setBody(" " + + " true" + + " " + + " denominator.io" + + " DNS Hosting" + + " ACTIVE" + + " 2015-09-29T13:58:53.000Z" + + " 2015-09-29T14:41:11.000Z" + + " " + + " nil@denominator.io" + + " 7400" + + " 86400" + + " 30000" + + " 1234567" + + " 1443535137" + + " " + + " COMPLETE" + + " " + + " false" + + " " + + " " + + " 10" + + " a1.verisigndns.com" + + " 209.112.113.33" + + " 2001:500:7967::2:33" + + " Anycast Global" + + " " + + " " + + " 11" + + " a2.verisigndns.com" + + " 209.112.114.33" + + " 2620:74:19::33" + + " Anycast 1" + + " " + + " " + + " 12" + + " a3.verisigndns.com" + + " 69.36.145.33" + + " 2001:502:cbe4::33" + + " Anycast 2" + + " " + + " " + " ")); + ZoneApi api = server.connect().api().zones(); + + assertThat(api.iterator()).containsExactly( + Zone.create("denominator.io", "denominator.io", 86400, "nil@denominator.io")); + } + + @Test + public void iteratorWhenAbsent() throws Exception { + server.enqueue(new MockResponse().setBody("")); + + ZoneApi api = server.connect().api().zones(); + assertThat(api.iterator()).isEmpty(); + } + + @Test + public void iterateByNameWhenPresent() throws Exception { + + server + .enqueue(new MockResponse() + .setBody(" " + + " true" + + " " + + " denominator.io" + + " DNS Hosting" + + " ACTIVE" + + " 2015-09-29T13:58:53.000Z" + + " 2015-09-29T14:41:11.000Z" + + " " + + " nil@denominator.io" + + " 7400" + + " 86400" + + " 30000" + + " 1234567" + + " 1443535137" + + " " + + " COMPLETE" + + " " + + " false" + + " " + + " " + + " 10" + + " a1.verisigndns.com" + + " 209.112.113.33" + + " 2001:500:7967::2:33" + + " Anycast Global" + + " " + + " " + + " 11" + + " a2.verisigndns.com" + + " 209.112.114.33" + + " 2620:74:19::33" + + " Anycast 1" + + " " + + " " + + " 12" + + " a3.verisigndns.com" + + " 69.36.145.33" + + " 2001:502:cbe4::33" + + " Anycast 2" + + " " + + " " + " ")); + ZoneApi api = server.connect().api().zones(); + + assertThat(api.iterateByName("denominator.io")).containsExactly( + Zone.create("denominator.io", "denominator.io", 86400, "nil@denominator.io")); + } + + @Test + public void iterateByNameWhenAbsent() throws Exception { + server.enqueue(new MockResponse().setBody("")); + + ZoneApi api = server.connect().api().zones(); + assertThat(api.iterateByName("denominator.io.")).isEmpty(); + } + + @Test + public void putWhenPresent() throws Exception { + server.enqueueError("ERROR_OPERATION_FAILURE", + "Domain already exists. Please verify your domain name."); + server.enqueue(new MockResponse()); + + ZoneApi api = server.connect().api().zones(); + + Zone zone = Zone.create("denominator.io", "denominator.io", 86400, "nil@denominator.io"); + api.put(zone); + } + + @Test + public void putWhenAbsent() throws Exception { + server.enqueue(new MockResponse()); + server.enqueue(new MockResponse()); + ZoneApi api = server.connect().api().zones(); + + Zone zone = Zone.create("denominator.io", "denominator.io", 86400, "nil@denominator.io"); + assertThat(api.put(zone)).isEqualTo(zone.name()); + } + + @Test + public void deleteWhenPresent() throws Exception { + server + .enqueue(new MockResponse() + .setBody("" + + " true" + + "")); + + ZoneApi api = server.connect().api().zones(); + api.delete("denominator.io."); + } + + @Test + public void deleteWhenAbsent() throws Exception { + server.enqueue(new MockResponse()); + + ZoneApi api = server.connect().api().zones(); + api.delete("test.io"); + } +} diff --git a/verisigndns/src/test/java/denominator/verisigndns/VerisignDnsZoneWriteCommandsLiveTest.java b/verisigndns/src/test/java/denominator/verisigndns/VerisignDnsZoneWriteCommandsLiveTest.java new file mode 100644 index 00000000..44952def --- /dev/null +++ b/verisigndns/src/test/java/denominator/verisigndns/VerisignDnsZoneWriteCommandsLiveTest.java @@ -0,0 +1,9 @@ +package denominator.verisigndns; + +import denominator.ZoneWriteCommandsLiveTest; +import denominator.Live.UseTestGraph; + +@UseTestGraph(VerisignDnsTestGraph.class) +public class VerisignDnsZoneWriteCommandsLiveTest extends ZoneWriteCommandsLiveTest { + +}