Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CONTRIBUTORS
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ Paul Woodland
Ross Pace
Hogne Vevle
Laurent Signorel
Esselink-nu (fork: host attributes, transfer response fields)
NoorDigitalAgency (fork: host info, poll domain names)
157 changes: 136 additions & 21 deletions EppLib.UnitTests/LocalTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -262,13 +262,9 @@ public void TestHostInfoCommand1()
[DeploymentItem("TestData/HostInfoResponse1.xml")]
public void TestHostInfoResponse1()
{
Assert.Inconclusive("Not implemented");

/*
byte[] input = File.ReadAllBytes("HostInfoResponse1.xml");
var response = new HostInfoResponse(input);
//var host = response.Host;
var host = new Host();
var host = response.Host;

Assert.AreEqual("1000", response.Code);
Assert.AreEqual("Command completed successfully", response.Message);
Expand All @@ -283,24 +279,13 @@ public void TestHostInfoResponse1()
Assert.AreEqual("1999-12-03T09:00:00.0Z", host.UpDate);
Assert.AreEqual("2000-04-08T09:00:00.0Z", host.TrDate);

var status = new List<Status>()
{
new Status("linked", null),
new Status("clientUpdateProhibited", null)
};
CollectionAssert.AreEqual(status.ToArray(), host.Status.ToArray());
CollectionAssert.AreEqual(new[] { "linked", "clientUpdateProhibited" }, host.Status.Select(s => s.Type).ToArray());

var hosts = new List<HostAddress>()
{
new HostAddress("192.0.2.2", "v4"),
new HostAddress("192.0.2.29", "v4"),
new HostAddress("1080:0:0:0:8:800:200C:417A", "v6")
};
CollectionAssert.AreEqual(hosts.ToArray(), host.Addresses.ToArray());
CollectionAssert.AreEqual(new[] { "192.0.2.2", "192.0.2.29", "1080:0:0:0:8:800:200C:417A" }, host.Addresses.Select(a => a.IPAddress).ToArray());
CollectionAssert.AreEqual(new[] { "v4", "v4", "v6" }, host.Addresses.Select(a => a.IPVersion).ToArray());

Assert.AreEqual("ABC-12345", response.ClientTransactionId);
Assert.AreEqual("54322-XYZ", response.ServerTransactionId);
*/
}

#endregion
Expand Down Expand Up @@ -1126,7 +1111,17 @@ public void TestDomainTransferQueryCommand1()
[DeploymentItem("TestData/DomainTransferQueryResponse1.xml")]
public void TestDomainTransferQueryResponse1()
{
Assert.Inconclusive("Not implemented");
var response = new DomainTransferResponse(File.ReadAllBytes("DomainTransferQueryResponse1.xml"));
var result = response.DomainTransferResult;

Assert.AreEqual("1000", response.Code);
Assert.AreEqual("example.com", result.DomainName);
Assert.AreEqual("pending", result.TransferStatus);
Assert.AreEqual("ClientX", result.RequestClientId);
Assert.AreEqual("2000-06-06T22:00:00.0Z", result.RequestDate);
Assert.AreEqual("ClientY", result.ActionClientId);
Assert.AreEqual("2000-06-11T22:00:00.0Z", result.ActionDate);
Assert.AreEqual("2002-09-08T22:00:00.0Z", result.ExpirationDate);
}

#endregion
Expand All @@ -1152,7 +1147,17 @@ public void TestDomainTransferRequestCommand1()
[DeploymentItem("TestData/DomainTransferRequestResponse1.xml")]
public void TestDomainTransferRequestResponse1()
{
Assert.Inconclusive("Not implemented");
var response = new DomainTransferResponse(File.ReadAllBytes("DomainTransferRequestResponse1.xml"));
var result = response.DomainTransferResult;

Assert.AreEqual("1001", response.Code);
Assert.AreEqual("example.com", result.DomainName);
Assert.AreEqual("pending", result.TransferStatus);
Assert.AreEqual("ClientX", result.RequestClientId);
Assert.AreEqual("2000-06-08T22:00:00.0Z", result.RequestDate);
Assert.AreEqual("ClientY", result.ActionClientId);
Assert.AreEqual("2000-06-13T22:00:00.0Z", result.ActionDate);
Assert.AreEqual("2002-09-08T22:00:00.0Z", result.ExpirationDate);
}

#endregion
Expand Down Expand Up @@ -1254,5 +1259,115 @@ public void TestPollMsgsResponse2()
}

#endregion

#region Fork gaps (1.8.0)

/// <summary>
/// Domain create with name servers as host attributes (RFC 5731 hostAttr)
/// </summary>
[TestMethod]
[TestCategory("LocalCommand")]
public void TestDomainCreateHostAttributes()
{
var command = new DomainCreate("example.com", "jd1234");
command.NameServerAttributes.Add(new DomainHostAttribute("ns1.example.net"));
command.NameServerAttributes.Add(new DomainHostAttribute("ns1.example.com", new HostAddress("192.0.2.2", "v4"), new HostAddress("1080:0:0:0:8:800:200C:417A", "v6")));
command.TransactionId = "ABC-12345";

var xml = command.ToXml().InnerXml;

StringAssert.Contains(xml, "<domain:ns><domain:hostAttr><domain:hostName>ns1.example.net</domain:hostName></domain:hostAttr>" +
"<domain:hostAttr><domain:hostName>ns1.example.com</domain:hostName><domain:hostAddr ip=\"v4\">192.0.2.2</domain:hostAddr>" +
"<domain:hostAddr ip=\"v6\">1080:0:0:0:8:800:200C:417A</domain:hostAddr></domain:hostAttr></domain:ns>");
Assert.IsFalse(xml.Contains("hostObj"));
}

[TestMethod]
[TestCategory("LocalCommand")]
[ExpectedException(typeof(InvalidOperationException))]
public void TestDomainCreateRejectsHostObjectsAndAttributesTogether()
{
var command = new DomainCreate("example.com", "jd1234");
command.NameServers.Add("ns1.example.net");
command.NameServerAttributes.Add(new DomainHostAttribute("ns2.example.net"));

command.ToXml();
}

/// <summary>
/// Domain update adding and removing host attributes
/// </summary>
[TestMethod]
[TestCategory("LocalCommand")]
public void TestDomainUpdateHostAttributes()
{
var command = new DomainUpdate("example.com");
command.ToAdd.NameServerAttributes.Add(new DomainHostAttribute("ns2.example.com", new HostAddress("192.0.2.3", "v4")));
command.ToRemove.NameServerAttributes.Add(new DomainHostAttribute("ns1.example.com"));

var xml = command.ToXml().InnerXml;

StringAssert.Contains(xml, "<domain:add><domain:ns><domain:hostAttr><domain:hostName>ns2.example.com</domain:hostName><domain:hostAddr ip=\"v4\">192.0.2.3</domain:hostAddr></domain:hostAttr></domain:ns></domain:add>");
StringAssert.Contains(xml, "<domain:rem><domain:ns><domain:hostAttr><domain:hostName>ns1.example.com</domain:hostName></domain:hostAttr></domain:ns></domain:rem>");
}

/// <summary>
/// Domain info response with host attributes keeps the glue addresses
/// </summary>
[TestMethod]
[TestCategory("LocalResponse")]
public void TestDomainInfoResponseHostAttributes()
{
var xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" +
"<epp xmlns=\"urn:ietf:params:xml:ns:epp-1.0\"><response><result code=\"1000\"><msg>Command completed successfully</msg></result>" +
"<resData><domain:infData xmlns:domain=\"urn:ietf:params:xml:ns:domain-1.0\"><domain:name>example.com</domain:name><domain:roid>EXAMPLE1-REP</domain:roid>" +
"<domain:ns><domain:hostAttr><domain:hostName>ns1.example.com</domain:hostName><domain:hostAddr ip=\"v4\">192.0.2.2</domain:hostAddr><domain:hostAddr ip=\"v6\">1080:0:0:0:8:800:200C:417A</domain:hostAddr></domain:hostAttr>" +
"<domain:hostAttr><domain:hostName>ns2.example.net</domain:hostName></domain:hostAttr></domain:ns>" +
"<domain:clID>ClientX</domain:clID></domain:infData></resData><trID><clTRID>ABC-12345</clTRID><svTRID>54322-XYZ</svTRID></trID></response></epp>";

var response = new DomainInfoResponse(xml);
var domain = response.Domain;

CollectionAssert.AreEqual(new[] { "ns1.example.com", "ns2.example.net" }, domain.NameServers.ToArray());
Assert.AreEqual(2, domain.NameServerAttributes.Count);
Assert.AreEqual("ns1.example.com", domain.NameServerAttributes[0].HostName);
CollectionAssert.AreEqual(new[] { "192.0.2.2", "1080:0:0:0:8:800:200C:417A" }, domain.NameServerAttributes[0].Addresses.Select(a => a.IPAddress).ToArray());
CollectionAssert.AreEqual(new[] { "v4", "v6" }, domain.NameServerAttributes[0].Addresses.Select(a => a.IPVersion).ToArray());
Assert.AreEqual(0, domain.NameServerAttributes[1].Addresses.Count);
}

/// <summary>
/// Poll message with a standard pending action notification (RFC 5731 panData)
/// </summary>
[TestMethod]
[TestCategory("LocalResponse")]
[DeploymentItem("TestData/MessageDomainPendingComplete1.xml")]
public void TestPollDomainNameFromPanData()
{
var response = new PollResponse(File.ReadAllBytes("MessageDomainPendingComplete1.xml"));

Assert.AreEqual("1301", response.Code);
Assert.AreEqual("example.com", response.DomainName);
}

/// <summary>
/// Poll message with an IIS (.se/.nu) update notification wrapping domain:infData
/// </summary>
[TestMethod]
[TestCategory("LocalResponse")]
public void TestPollDomainNameFromIisUpdateNotify()
{
var xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" +
"<epp xmlns=\"urn:ietf:params:xml:ns:epp-1.0\"><response><result code=\"1301\"><msg>Command completed successfully; ack to dequeue</msg></result>" +
"<msgQ count=\"1\" id=\"77\"><qDate>2025-06-19T10:00:00.0Z</qDate><msg>Domain updated</msg></msgQ>" +
"<resData><iis:updateNotify xmlns:iis=\"urn:se:iis:xml:epp:iis-1.2\"><domain:infData xmlns:domain=\"urn:ietf:params:xml:ns:domain-1.0\"><domain:name>example.se</domain:name><domain:roid>example.se-1</domain:roid></domain:infData></iis:updateNotify></resData>" +
"<trID><svTRID>54322-XYZ</svTRID></trID></response></epp>";

var response = new PollResponse(System.Text.Encoding.UTF8.GetBytes(xml));

Assert.AreEqual("example.se", response.DomainName);
}

#endregion
}
}
7 changes: 7 additions & 0 deletions EppLib/Entities/Domain/Domain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class Domain
public Domain()
{
NameServers = new List<string>();
NameServerAttributes = new List<DomainHostAttribute>();
Hosts = new List<string>();
Status = new List<Status>();
Contacts = new List<DomainContact>();
Expand Down Expand Up @@ -56,6 +57,12 @@ public Domain()
/// </summary>
public IList<string> NameServers { get; set; }

/// <summary>
/// Name servers the registry returned as host attributes, with their glue addresses.
/// Their names are also in <see cref="NameServers"/>.
/// </summary>
public IList<DomainHostAttribute> NameServerAttributes { get; set; }

/// <summary>
/// The identifier of the sponsoring Registrar
/// </summary>
Expand Down
25 changes: 25 additions & 0 deletions EppLib/Entities/Domain/DomainBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,30 @@ protected XmlNode CreateNameServerElement(XmlDocument doc, IEnumerable<string> n

return nameServerElement;
}

/// <summary>
/// Builds domain:ns from either host object names (hostObj) or host attributes (hostAttr); RFC 5731 allows one form per element.
/// </summary>
protected XmlNode CreateNameServerElement(XmlDocument doc, ICollection<string> hostObjects, ICollection<DomainHostAttribute> hostAttributes)
{
if (hostObjects.Count > 0 && hostAttributes.Count > 0)
{
throw new InvalidOperationException("domain:ns takes either host objects (NameServers) or host attributes (NameServerAttributes), not both (RFC 5731 section 1.1).");
}

if (hostObjects.Count > 0)
{
return CreateNameServerElement(doc, hostObjects);
}

var nameServerElement = doc.CreateElement("domain:ns", namespaceUri);

foreach (var hostAttribute in hostAttributes)
{
nameServerElement.AppendChild(hostAttribute.ToXml(doc, namespaceUri));
}

return nameServerElement;
}
}
}
10 changes: 8 additions & 2 deletions EppLib/Entities/Domain/DomainCreate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ public IList<string> NameServers
get { return nameServers; }
}

/// <summary>
/// Name servers as host attributes (name plus glue addresses), for registries that do not use host objects.
/// Use this or <see cref="NameServers"/>, not both.
/// </summary>
public IList<DomainHostAttribute> NameServerAttributes { get; } = new List<DomainHostAttribute>();

public IList<DomainContact> DomainContacts
{
get { return domainContacts; }
Expand All @@ -58,9 +64,9 @@ protected override XmlElement BuildCommandElement(XmlDocument doc, XmlElement co
period.SetAttribute("unit", Period.Unit);
}

if (NameServers != null && NameServers.Count>0)
if (NameServers.Count > 0 || NameServerAttributes.Count > 0)
{
domainCreate.AppendChild(CreateNameServerElement(doc, NameServers));
domainCreate.AppendChild(CreateNameServerElement(doc, NameServers, NameServerAttributes));
}

if (RegistrantContactId != null)
Expand Down
67 changes: 67 additions & 0 deletions EppLib/Entities/Domain/DomainHostAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2012 Code Maker Inc. (http://codemaker.net)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System.Collections.Generic;
using System.Xml;

namespace EppLib.Entities
{
/// <summary>
/// A name server given by name and, for hosts inside the domain, IP addresses: the domain:hostAttr
/// form of RFC 5731. Registries that do not use separate host objects expect this form.
/// </summary>
public class DomainHostAttribute
{
public DomainHostAttribute()
{
Addresses = new List<HostAddress>();
}

public DomainHostAttribute(string hostName, params HostAddress[] addresses)
{
HostName = hostName;
Addresses = new List<HostAddress>(addresses);
}

public string HostName { get; set; }

/// <summary>
/// Glue addresses; set IPVersion to "v4" or "v6".
/// </summary>
public IList<HostAddress> Addresses { get; private set; }

internal XmlElement ToXml(XmlDocument doc, string namespaceUri)
{
var hostAttr = doc.CreateElement("domain:hostAttr", namespaceUri);

var hostName = doc.CreateElement("domain:hostName", namespaceUri);
hostName.InnerText = HostName;
hostAttr.AppendChild(hostName);

foreach (var address in Addresses)
{
var hostAddr = doc.CreateElement("domain:hostAddr", namespaceUri);
hostAddr.InnerText = address.IPAddress;

if (address.IPVersion != null)
{
hostAddr.SetAttribute("ip", address.IPVersion);
}

hostAttr.AppendChild(hostAddr);
}

return hostAttr;
}
}
}
15 changes: 15 additions & 0 deletions EppLib/Entities/Domain/DomainInfoResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
{
public class DomainInfoResponse : EppResponse
{
protected Domain _domain = new Domain();

Check warning on line 20 in EppLib/Entities/Domain/DomainInfoResponse.cs

View workflow job for this annotation

GitHub Actions / build and test

Identifier '_domain' is not CLS-compliant

Check warning on line 20 in EppLib/Entities/Domain/DomainInfoResponse.cs

View workflow job for this annotation

GitHub Actions / build and test

Identifier '_domain' is not CLS-compliant
public virtual Domain Domain
{
get { return _domain; }
Expand Down Expand Up @@ -136,6 +136,21 @@
Domain.NameServers.Add(hostName.InnerText);
}
}

var hostNameNode = hostAttrNode.SelectSingleNode("domain:hostName", namespaces);

if (hostNameNode != null)
{
var hostAttribute = new DomainHostAttribute { HostName = hostNameNode.InnerText };

foreach (XmlNode hostAddrNode in hostAttrNode.SelectNodes("domain:hostAddr", namespaces))
{
var ip = hostAddrNode.Attributes?["ip"];
hostAttribute.Addresses.Add(new HostAddress(hostAddrNode.InnerText, ip != null ? ip.Value : "v4"));
}

Domain.NameServerAttributes.Add(hostAttribute);
}
}
}
}
Expand Down
9 changes: 8 additions & 1 deletion EppLib/Entities/Domain/DomainTransferResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,19 @@ protected override void ProcessDataNode(XmlDocument doc, XmlNamespaceManager nam
DomainTransferResult.CreatedDate = crDateNode.InnerText;
}

var exDateNode = children.SelectSingleNode("domain:expDate", namespaces);
// RFC 5731 names it exDate; expDate is kept for registries that relied on the old lookup.
var exDateNode = children.SelectSingleNode("domain:exDate", namespaces) ?? children.SelectSingleNode("domain:expDate", namespaces);

if (exDateNode != null)
{
DomainTransferResult.ExpirationDate = exDateNode.InnerText;
}

DomainTransferResult.TransferStatus = children.SelectSingleNode("domain:trStatus", namespaces)?.InnerText;
DomainTransferResult.RequestClientId = children.SelectSingleNode("domain:reID", namespaces)?.InnerText;
DomainTransferResult.RequestDate = children.SelectSingleNode("domain:reDate", namespaces)?.InnerText;
DomainTransferResult.ActionClientId = children.SelectSingleNode("domain:acID", namespaces)?.InnerText;
DomainTransferResult.ActionDate = children.SelectSingleNode("domain:acDate", namespaces)?.InnerText;
}
}
}
Expand Down
Loading
Loading