diff --git a/README.md b/README.md
index 47f458a..ed8207e 100644
--- a/README.md
+++ b/README.md
@@ -15,35 +15,9 @@ EPP ([Extensible Provisioning Protocol](https://tools.ietf.org/html/rfc5730)) cl
## Usage
```go
-tconn, err := tls.Dial("tcp", "epp.example.com:700", nil)
-if err != nil {
- return err
-}
-
-conn, err := epp.NewConn(tconn)
-if err != nil {
- return err
-}
-
-err = conn.Login(user, password, "")
-if err != nil {
- return err
-}
-
-dcr, err := conn.CheckDomain("google.com")
-if err != nil {
- return err
-}
-for _, r := range dcr.Checks {
- // ...
-}
+// TODO: document new API
```
-## Todo
-
-- [X] Tests
-- [ ] Commands other than `Check`
-
## Author
© 2021 nb.io LLC
diff --git a/bool_test.go b/bool_test.go
deleted file mode 100644
index 25b06bd..0000000
--- a/bool_test.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package epp
-
-import (
- "encoding/xml"
- "testing"
-
- "github.com/nbio/st"
-)
-
-func TestBool(t *testing.T) {
- x := []byte(``)
- var y struct {
- XMLName struct{} `xml:"example"`
- Fred Bool `xml:"fred"`
- Jane Bool `xml:"jane"`
- Susan Bool `xml:"susan"`
- }
-
- err := xml.Unmarshal(x, &y)
- st.Expect(t, err, nil)
- st.Expect(t, y.Fred, True)
- st.Expect(t, y.Jane, False)
- st.Expect(t, y.Susan, True)
- z, err := xml.Marshal(&y)
- st.Expect(t, err, nil)
- st.Expect(t, string(z), ``)
-}
-
-func TestBoolAttr(t *testing.T) {
- x := []byte(``)
- var y struct {
- XMLName struct{} `xml:"example"`
- Fred Bool `xml:"fred,attr"`
- Jane Bool `xml:"jane,attr"`
- Susan Bool `xml:"susan,attr"`
- }
-
- err := xml.Unmarshal(x, &y)
- st.Expect(t, err, nil)
- st.Expect(t, y.Fred, True)
- st.Expect(t, y.Jane, False)
- st.Expect(t, y.Susan, False)
- z, err := xml.Marshal(&y)
- st.Expect(t, err, nil)
- st.Expect(t, string(z), ``)
-}
diff --git a/check.go b/check.go
deleted file mode 100644
index 26c9bee..0000000
--- a/check.go
+++ /dev/null
@@ -1,457 +0,0 @@
-package epp
-
-import (
- "bytes"
- "encoding/xml"
- "fmt"
- "strings"
-
- "github.com/nbio/xx"
-)
-
-// CheckDomain queries the EPP server for the availability status of one or more domains.
-func (c *Conn) CheckDomain(domains ...string) (*DomainCheckResponse, error) {
- return c.CheckDomainExtensions(domains, nil)
-}
-
-// CheckDomainExtensions allows specifying extension data for the following:
-// - "neulevel:unspec": a string of the Key=Value data for the unspec tag
-// - "launch:phase": a string of the launch phase
-func (c *Conn) CheckDomainExtensions(domains []string, extData map[string]string) (*DomainCheckResponse, error) {
- x, err := encodeDomainCheck(&c.Greeting, domains, extData)
- if err != nil {
- return nil, err
- }
-
- err = c.writeRequest(x)
- if err != nil {
- return nil, err
- }
-
- res, err := c.readResponse()
- if err != nil {
- return nil, err
- }
-
- // The ARI price extension won't return both availability and price data
- // in the same response, so we have to make a separate request for price
- if c.Greeting.SupportsExtension(ExtPrice) {
- x, err = encodePriceCheck(domains)
- if err != nil {
- return nil, err
- }
- err = c.writeRequest(x)
- if err != nil {
- return nil, err
- }
- res2, err := c.readResponse()
- if err != nil {
- return nil, err
- }
- res.DomainCheckResponse.Charges = res2.DomainCheckResponse.Charges
- }
-
- return &res.DomainCheckResponse, nil
-
-}
-
-func encodeDomainCheck(greeting *Greeting, domains []string, extData map[string]string) ([]byte, error) {
- buf := bytes.NewBufferString(xmlCommandPrefix)
- buf.WriteString(``)
- buf.WriteString(``)
- for _, domain := range domains {
- buf.WriteString(``)
- xml.EscapeText(buf, []byte(domain))
- buf.WriteString(``)
- }
- buf.WriteString(``)
- buf.WriteString(``)
-
- var feeURN string
- switch {
- case greeting.SupportsExtension(ExtFee10):
- feeURN = ExtFee10
- case greeting.SupportsExtension(ExtFee21):
- feeURN = ExtFee21
- case greeting.SupportsExtension(ExtFee11):
- feeURN = ExtFee11
- // Versions 0.8-0.9 require the returned class to be "standard" for
- // non-premium domains
- case greeting.SupportsExtension(ExtFee08):
- feeURN = ExtFee08
- case greeting.SupportsExtension(ExtFee09):
- feeURN = ExtFee09
- // Version 0.5 has an attribute premium="1" for premium domains
- case greeting.SupportsExtension(ExtFee05):
- feeURN = ExtFee05
- // Version 0.6 and 0.7 don't have a standard way of detecting premiums,
- // so instead there must be matching done on class names
- case greeting.SupportsExtension(ExtFee06):
- feeURN = ExtFee06
- case greeting.SupportsExtension(ExtFee07):
- feeURN = ExtFee07
- }
-
- supportsLaunch := extData["launch:phase"] != "" && greeting.SupportsExtension(ExtLaunch)
- supportsFeePhase := extData["fee:phase"] != ""
- supportsNeulevel := extData["neulevel:unspec"] != "" && (greeting.SupportsExtension(ExtNeulevel) || greeting.SupportsExtension(ExtNeulevel10))
- supportsNamestore := extData["namestoreExt:subProduct"] != "" && greeting.SupportsExtension(ExtNamestore)
-
- hasExtension := feeURN != "" || supportsLaunch || supportsNeulevel || supportsNamestore
-
- if hasExtension {
- buf.WriteString(``)
- }
-
- // https://www.verisign.com/assets/epp-sdk/verisign_epp-extension_namestoreext_v01.html
- if supportsNamestore {
- buf.WriteString(``)
- buf.WriteString(``)
- buf.WriteString(extData["namestoreExt:subProduct"])
- buf.WriteString(``)
- buf.WriteString(``)
- }
-
- if supportsLaunch {
- buf.WriteString(``)
- buf.WriteString(``)
- buf.WriteString(extData["launch:phase"])
- buf.WriteString(``)
- buf.WriteString(``)
- }
-
- if supportsNeulevel {
- buf.WriteString(``)
- buf.WriteString(``)
- buf.WriteString(extData["neulevel:unspec"])
- buf.WriteString(``)
- buf.WriteString(``)
- }
-
- if len(feeURN) > 0 {
- buf.WriteString(``)
- feePhase := ""
- if supportsFeePhase {
- feePhase = fmt.Sprintf(" phase=%q", extData["fee:phase"])
- }
- for _, domain := range domains {
- switch feeURN {
- case ExtFee09: // Version 0.9 changes the XML structure
- buf.WriteString(``)
- buf.WriteString(``)
- xml.EscapeText(buf, []byte(domain))
- buf.WriteString(``)
- buf.WriteString(fmt.Sprintf(`create`, feePhase))
- buf.WriteString(``)
- case ExtFee11: // https://tools.ietf.org/html/draft-brown-epp-fees-07#section-5.1.1
- buf.WriteString(fmt.Sprintf(`create`, feePhase))
- case ExtFee21: // Version 0.21 changes the XML structure
- buf.WriteString(``)
- case ExtFee10:
- buf.WriteString(``)
- default:
- buf.WriteString(``)
- buf.WriteString(``)
- xml.EscapeText(buf, []byte(domain))
- buf.WriteString(``)
- buf.WriteString(fmt.Sprintf(`create`, feePhase))
- buf.WriteString(``)
- }
- }
- buf.WriteString(``)
- }
-
- if hasExtension {
- buf.WriteString(``)
- }
-
- buf.WriteString(xmlCommandSuffix)
-
- return buf.Bytes(), nil
-}
-
-func encodePriceCheck(domains []string) ([]byte, error) {
- buf := bytes.NewBufferString(xmlCommandPrefix)
- buf.WriteString(``)
- buf.WriteString(``)
- for _, domain := range domains {
- buf.WriteString(``)
- xml.EscapeText(buf, []byte(domain))
- buf.WriteString(``)
- }
- buf.WriteString(``)
- buf.WriteString(``)
- // Extensions
- buf.WriteString(``)
- // ARI price extension
- buf.WriteString(``)
- buf.WriteString(``)
- buf.WriteString(xmlCommandSuffix)
- return buf.Bytes(), nil
-}
-
-// DomainCheckResponse represents an EPP for a domain check.
-type DomainCheckResponse struct {
- Domain string
- Checks []DomainCheck
- Charges []DomainCharge
-}
-
-// DomainCheck represents an EPP and associated extension data.
-type DomainCheck struct {
- Domain string
- Reason string
- Available bool
-}
-
-// DomainCharge represents various EPP charge and fee extension data.
-// FIXME: unpack into multiple types for different extensions.
-type DomainCharge struct {
- Domain string
- Category string
- CategoryName string
-}
-
-func init() {
- // Default EPP check data
- path := "epp > response > resData > " + ObjDomain + " chkData"
- scanResponse.MustHandleStartElement(path+">cd", func(c *xx.Context) error {
- dcr := &c.Value.(*Response).DomainCheckResponse
- dcr.Checks = append(dcr.Checks, DomainCheck{})
- return nil
- })
- scanResponse.MustHandleCharData(path+">cd>name", func(c *xx.Context) error {
- checks := c.Value.(*Response).DomainCheckResponse.Checks
- check := &checks[len(checks)-1]
- check.Domain = string(c.CharData)
- check.Available = c.AttrBool("", "avail")
- return nil
- })
- scanResponse.MustHandleCharData(path+">cd>reason", func(c *xx.Context) error {
- checks := c.Value.(*Response).DomainCheckResponse.Checks
- check := &checks[len(checks)-1]
- check.Reason = string(c.CharData)
- return nil
- })
-
- // Scan charge-1.0 extension into Charges
- path = "epp > response > extension > " + ExtCharge + " chkData"
- scanResponse.MustHandleCharData(path+">cd>name", func(c *xx.Context) error {
- c.Value.(*Response).DomainCheckResponse.Domain = string(c.CharData)
- return nil
- })
- scanResponse.MustHandleStartElement(path+">cd>set", func(c *xx.Context) error {
- dcr := &c.Value.(*Response).DomainCheckResponse
- dcr.Charges = append(dcr.Charges, DomainCharge{})
- return nil
- })
- scanResponse.MustHandleCharData(path+">cd>set>category", func(c *xx.Context) error {
- charges := c.Value.(*Response).DomainCheckResponse.Charges
- charge := &charges[len(charges)-1]
- charge.Domain = c.Value.(*Response).DomainCheckResponse.Domain
- charge.Category = string(c.CharData)
- charge.CategoryName = c.Attr("", "name")
- return nil
- })
-
- path = "epp > response > extension > " + ExtFee05 + " chkData"
- scanResponse.MustHandleStartElement(path+">cd", func(c *xx.Context) error {
- dcr := &c.Value.(*Response).DomainCheckResponse
- dcr.Charges = append(dcr.Charges, DomainCharge{})
- return nil
- })
- scanResponse.MustHandleCharData(path+">cd>name", func(c *xx.Context) error {
- charges := c.Value.(*Response).DomainCheckResponse.Charges
- charge := &charges[len(charges)-1]
- charge.Domain = string(c.CharData)
- if c.AttrBool("", "premium") {
- charge.Category = "premium"
- }
- return nil
- })
- scanResponse.MustHandleCharData(path+">cd>class", func(c *xx.Context) error {
- charges := c.Value.(*Response).DomainCheckResponse.Charges
- charge := &charges[len(charges)-1]
- charge.Category = string(c.CharData)
- return nil
- })
- scanResponse.MustHandleCharData(path+">cd>fee", func(c *xx.Context) error {
- charges := c.Value.(*Response).DomainCheckResponse.Charges
- charge := &charges[len(charges)-1]
- charge.CategoryName = c.Attr("", "description")
- return nil
- })
-
- path = "epp > response > extension > " + ExtFee06 + " chkData"
- scanResponse.MustHandleStartElement(path+">cd", func(c *xx.Context) error {
- dcr := &c.Value.(*Response).DomainCheckResponse
- dcr.Charges = append(dcr.Charges, DomainCharge{})
- return nil
- })
- scanResponse.MustHandleCharData(path+">cd>name", func(c *xx.Context) error {
- charges := c.Value.(*Response).DomainCheckResponse.Charges
- charge := &charges[len(charges)-1]
- charge.Domain = string(c.CharData)
- return nil
- })
- scanResponse.MustHandleCharData(path+">cd>class", func(c *xx.Context) error {
- charges := c.Value.(*Response).DomainCheckResponse.Charges
- charge := &charges[len(charges)-1]
- className := strings.ToLower(string(c.CharData))
- isDefault := strings.Contains(className, "default")
- isNormal := strings.Contains(className, "normal")
- isDiscount := strings.Contains(className, "discount")
- //lint:ignore S1002 keep == false for clarity
- if isDefault == false && isNormal == false && isDiscount == false {
- charge.Category = "premium"
- }
- return nil
- })
-
- path = "epp > response > extension > " + ExtFee07 + " chkData"
- scanResponse.MustHandleStartElement(path+">cd", func(c *xx.Context) error {
- dcr := &c.Value.(*Response).DomainCheckResponse
- dcr.Charges = append(dcr.Charges, DomainCharge{})
- return nil
- })
- scanResponse.MustHandleCharData(path+">cd>name", func(c *xx.Context) error {
- charges := c.Value.(*Response).DomainCheckResponse.Charges
- charge := &charges[len(charges)-1]
- charge.Domain = string(c.CharData)
- return nil
- })
- scanResponse.MustHandleCharData(path+">cd>class", func(c *xx.Context) error {
- charges := c.Value.(*Response).DomainCheckResponse.Charges
- charge := &charges[len(charges)-1]
- charge.Category = string(c.CharData)
- return nil
- })
-
- path = "epp > response > extension > " + ExtFee08 + " chkData"
- scanResponse.MustHandleStartElement(path+">cd", func(c *xx.Context) error {
- dcr := &c.Value.(*Response).DomainCheckResponse
- dcr.Charges = append(dcr.Charges, DomainCharge{})
- return nil
- })
- scanResponse.MustHandleCharData(path+">cd>name", func(c *xx.Context) error {
- charges := c.Value.(*Response).DomainCheckResponse.Charges
- charge := &charges[len(charges)-1]
- charge.Domain = string(c.CharData)
- return nil
- })
- scanResponse.MustHandleCharData(path+">cd>class", func(c *xx.Context) error {
- charges := c.Value.(*Response).DomainCheckResponse.Charges
- charge := &charges[len(charges)-1]
- if string(c.CharData) != "standard" {
- charge.Category = "premium"
- }
- return nil
- })
-
- path = "epp > response > extension > " + ExtFee09 + " chkData"
- scanResponse.MustHandleStartElement(path+">cd", func(c *xx.Context) error {
- dcr := &c.Value.(*Response).DomainCheckResponse
- dcr.Charges = append(dcr.Charges, DomainCharge{})
- return nil
- })
- scanResponse.MustHandleCharData(path+">cd>objID", func(c *xx.Context) error {
- charges := c.Value.(*Response).DomainCheckResponse.Charges
- charge := &charges[len(charges)-1]
- charge.Domain = string(c.CharData)
- return nil
- })
- scanResponse.MustHandleCharData(path+">cd>class", func(c *xx.Context) error {
- charges := c.Value.(*Response).DomainCheckResponse.Charges
- charge := &charges[len(charges)-1]
- if string(c.CharData) != "standard" {
- charge.Category = "premium"
- }
- return nil
- })
-
- path = "epp > response > extension > " + ExtFee11 + " chkData"
- scanResponse.MustHandleStartElement(path+">cd", func(c *xx.Context) error {
- dcr := &c.Value.(*Response).DomainCheckResponse
- dcr.Charges = append(dcr.Charges, DomainCharge{})
- return nil
- })
- scanResponse.MustHandleCharData(path+">cd>objID", func(c *xx.Context) error {
- charges := c.Value.(*Response).DomainCheckResponse.Charges
- charge := &charges[len(charges)-1]
- charge.Domain = string(c.CharData)
- return nil
- })
- scanResponse.MustHandleCharData(path+">cd>class", func(c *xx.Context) error {
- charges := c.Value.(*Response).DomainCheckResponse.Charges
- charge := &charges[len(charges)-1]
- charge.Category = string(c.CharData)
- return nil
- })
-
- // Scan fee-0.21 phase and subphase into Charges Category and CategoryName, respectively
- // FIXME: stop mangling fee extensions into charges
- path = "epp > response > extension > " + ExtFee21 + " chkData > cd > command > fee"
- scanResponse.MustHandleCharData(path, func(c *xx.Context) error {
- if c.Parent.Attr("", "name") != "create" {
- return nil
- }
- dcr := &c.Value.(*Response).DomainCheckResponse
- check := &dcr.Checks[len(dcr.Checks)-1]
- charge := DomainCharge{
- Domain: check.Domain,
- Category: c.Parent.Attr("", "phase"),
- CategoryName: c.Parent.Attr("", "subphase"),
- }
- dcr.Charges = append(dcr.Charges, charge)
- return nil
- })
-
- // Scan price-1.1 extension into Charges
- path = "epp > response > extension > " + ExtPrice + " chkData"
- scanResponse.MustHandleStartElement(path+">cd", func(c *xx.Context) error {
- dcr := &c.Value.(*Response).DomainCheckResponse
- dcr.Charges = append(dcr.Charges, DomainCharge{})
- return nil
- })
- scanResponse.MustHandleCharData(path+">cd>name", func(c *xx.Context) error {
- charges := c.Value.(*Response).DomainCheckResponse.Charges
- charge := &charges[len(charges)-1]
- charge.Domain = string(c.CharData)
- if c.AttrBool("", "premium") {
- charge.Category = "premium"
- }
- return nil
- })
-
- // Scan neulevel-1.0 extension
- path = "epp > response > extension > " + ExtNeulevel10 + " extension > unspec"
- scanResponse.MustHandleCharData(path, func(c *xx.Context) error {
- dcr := &c.Value.(*Response).DomainCheckResponse
- if len(dcr.Checks) == 0 {
- return nil
- }
-
- check := &dcr.Checks[len(dcr.Checks)-1]
- charge := DomainCharge{Domain: check.Domain}
- data := string(c.CharData)
- pairs := strings.Split(data, " ")
- for _, pair := range pairs {
- parts := strings.SplitN(pair, "=", 2)
- if len(parts) == 2 && parts[0] == "TierName" {
- charge.Category = parts[1]
- break
- }
- }
- dcr.Charges = append(dcr.Charges, charge)
-
- return nil
- })
-}
diff --git a/check_test.go b/check_test.go
deleted file mode 100644
index ce03626..0000000
--- a/check_test.go
+++ /dev/null
@@ -1,807 +0,0 @@
-package epp
-
-import (
- "bytes"
- "encoding/xml"
- "testing"
-
- "github.com/nbio/st"
-)
-
-func decoder(s string) *xml.Decoder {
- return xml.NewDecoder(bytes.NewBufferString(s))
-}
-
-func TestConnCheck(t *testing.T) {
- t.Skip("no live EPP tests in the test suite")
- c, err := NewConn(nil)
- st.Expect(t, err, nil)
- st.Reject(t, c, nil)
- dcr, err := c.CheckDomain("google.com")
- st.Expect(t, err, nil)
- st.Reject(t, dcr, nil)
- st.Expect(t, len(dcr.Checks), 1)
- st.Expect(t, dcr.Checks[0].Domain, "google.com")
- st.Expect(t, dcr.Checks[0].Available, false)
-
- dcr, err = c.CheckDomain("dmnr-test-x759824vim-i2.com")
- st.Expect(t, err, nil)
- st.Reject(t, dcr, nil)
- st.Expect(t, len(dcr.Checks), 1)
- st.Expect(t, dcr.Checks[0].Domain, "dmnr-test-x759824vim-i2.com")
- st.Expect(t, dcr.Checks[0].Available, true)
-
- dcr, err = c.CheckDomain("--dmnr-test--.com")
- st.Reject(t, err, nil)
- st.Expect(t, dcr, (*DomainCheckResponse)(nil))
-}
-
-func TestEncodeDomainCheck(t *testing.T) {
- x, err := encodeDomainCheck(nil, []string{"hello.com", "foo.domains", "xn--ninja.net"}, nil)
- st.Expect(t, err, nil)
- st.Expect(t, string(x), `
-hello.comfoo.domainsxn--ninja.net`)
- var v struct{}
- err = xml.Unmarshal(x, &v)
- st.Expect(t, err, nil)
-}
-
-func TestEncodeDomainCheckLaunchPhase(t *testing.T) {
- var greeting Greeting
- greeting.Extensions = []string{ExtLaunch}
- x, err := encodeDomainCheck(&greeting, []string{"hello.com", "foo.domains", "xn--ninja.net"}, map[string]string{"launch:phase": "claims"})
- st.Expect(t, err, nil)
- st.Expect(t, string(x), `
-hello.comfoo.domainsxn--ninja.netclaims`)
- var v struct{}
- err = xml.Unmarshal(x, &v)
- st.Expect(t, err, nil)
-}
-
-func TestEncodeDomainCheckNeulevelUnspec(t *testing.T) {
- var greeting Greeting
- greeting.Extensions = []string{ExtNeulevel}
- x, err := encodeDomainCheck(&greeting, []string{"hello.com", "foo.domains", "xn--ninja.net"}, map[string]string{"neulevel:unspec": "FeeCheck=Y"})
- st.Expect(t, err, nil)
- st.Expect(t, string(x), `
-hello.comfoo.domainsxn--ninja.netFeeCheck=Y`)
- var v struct{}
- err = xml.Unmarshal(x, &v)
- st.Expect(t, err, nil)
-}
-
-func TestScanCheckDomainResponseWithCharge(t *testing.T) {
- x := `
-
-
-
- Command completed successfully
-
-
-
-
- good.memorial
- premium name
-
-
-
-
-
-
- good.memorial
-
- premium
- price
- 100.00
- 100.00
- 100.00
- 50.00
-
-
-
-
-
- 0000000000000002
- 83fa5767-5624-4be5-9e54-0b3a52f9de5b:1
-
-
-`
-
- var res Response
- dcr := &res.DomainCheckResponse
-
- d := decoder(x)
- err := IgnoreEOF(scanResponse.Scan(d, &res))
- st.Expect(t, err, nil)
- st.Expect(t, len(dcr.Checks), 1)
- st.Expect(t, dcr.Checks[0].Domain, "good.memorial")
- st.Expect(t, dcr.Checks[0].Available, true)
- st.Expect(t, dcr.Checks[0].Reason, "premium name")
- st.Expect(t, len(dcr.Charges), 1)
- st.Expect(t, dcr.Charges[0].Domain, "good.memorial")
- st.Expect(t, dcr.Charges[0].Category, "premium")
- st.Expect(t, dcr.Charges[0].CategoryName, "BBB+")
-}
-func TestScanCheckDomainResponseWithMultipleChargeSets(t *testing.T) {
- x := `
-
-
-
- Command completed successfully
-
-
-
-
- good.memorial
- premium name
-
-
-
-
-
-
- good.memorial
-
- premium
- price
- 100.00
- 100.00
- 100.00
- 50.00
-
-
- earlyAccess
- fee
- 2500.00
-
-
-
-
-
- 0000000000000002
- 83fa5767-5624-4be5-9e54-0b3a52f9de5b:1
-
-
-`
-
- var res Response
- dcr := &res.DomainCheckResponse
-
- d := decoder(x)
- err := IgnoreEOF(scanResponse.Scan(d, &res))
- st.Expect(t, err, nil)
- st.Expect(t, len(dcr.Checks), 1)
- st.Expect(t, dcr.Checks[0].Domain, "good.memorial")
- st.Expect(t, dcr.Checks[0].Available, true)
- st.Expect(t, dcr.Checks[0].Reason, "premium name")
- st.Expect(t, len(dcr.Charges), 2)
- st.Expect(t, dcr.Charges[0].Domain, "good.memorial")
- st.Expect(t, dcr.Charges[0].Category, "premium")
- st.Expect(t, dcr.Charges[0].CategoryName, "BBB+")
- st.Expect(t, dcr.Charges[1].Domain, "good.memorial")
- st.Expect(t, dcr.Charges[1].Category, "earlyAccess")
- st.Expect(t, dcr.Charges[1].CategoryName, "")
-}
-
-func TestScanCheckDomainResponseWithFee05(t *testing.T) {
- x := `
-
-
-
- Command completed successfully
-
-
-
-
- good.space
-
-
-
-
-
-
- good.space
- USD
- create
- 1
- 100.00
- premium
-
-
-
-
- 0000000000000002
- 83fa5767-5624-4be5-9e54-0b3a52f9de5b:1
-
-
-`
-
- var res Response
- dcr := &res.DomainCheckResponse
-
- d := decoder(x)
- err := IgnoreEOF(scanResponse.Scan(d, &res))
- st.Expect(t, err, nil)
- st.Expect(t, len(dcr.Checks), 1)
- st.Expect(t, dcr.Checks[0].Domain, "good.space")
- st.Expect(t, dcr.Checks[0].Available, true)
- st.Expect(t, dcr.Checks[0].Reason, "")
- st.Expect(t, len(dcr.Charges), 1)
- st.Expect(t, dcr.Charges[0].Domain, "good.space")
- st.Expect(t, dcr.Charges[0].Category, "premium")
- st.Expect(t, dcr.Charges[0].CategoryName, "Premium Registration Fee")
-}
-
-func TestScanCheckDomainResponseWithFee06(t *testing.T) {
- x := `
-
-
-
- Command completed successfully
-
-
-
-
- good.space
-
-
-
-
-
-
- good.space
- USD
- create
- 1
- 100.00
- SPACE Tier 1
-
-
-
-
- 0000000000000002
- 83fa5767-5624-4be5-9e54-0b3a52f9de5b:1
-
-
-`
-
- var res Response
- dcr := &res.DomainCheckResponse
-
- d := decoder(x)
- err := IgnoreEOF(scanResponse.Scan(d, &res))
- st.Expect(t, err, nil)
- st.Expect(t, len(dcr.Checks), 1)
- st.Expect(t, dcr.Checks[0].Domain, "good.space")
- st.Expect(t, dcr.Checks[0].Available, true)
- st.Expect(t, dcr.Checks[0].Reason, "")
- st.Expect(t, len(dcr.Charges), 1)
- st.Expect(t, dcr.Charges[0].Domain, "good.space")
- st.Expect(t, dcr.Charges[0].Category, "premium")
- st.Expect(t, dcr.Charges[0].CategoryName, "")
-}
-
-func TestScanCheckDomainResponseWithFee07(t *testing.T) {
- x := `
-
-
-
- Command completed successfully
-
-
-
-
- austin.green
-
-
-
-
-
-
- austin.green
- USD
- create
- 1
- 3500.00
- premium
-
-
-
-
- 0000000000000002
- 83fa5767-5624-4be5-9e54-0b3a52f9de5b:1
-
-
-`
-
- var res Response
- dcr := &res.DomainCheckResponse
-
- d := decoder(x)
- err := IgnoreEOF(scanResponse.Scan(d, &res))
- st.Expect(t, err, nil)
- st.Expect(t, len(dcr.Checks), 1)
- st.Expect(t, dcr.Checks[0].Domain, "austin.green")
- st.Expect(t, dcr.Checks[0].Available, true)
- st.Expect(t, dcr.Checks[0].Reason, "")
- st.Expect(t, len(dcr.Charges), 1)
- st.Expect(t, dcr.Charges[0].Domain, "austin.green")
- st.Expect(t, dcr.Charges[0].Category, "premium")
- st.Expect(t, dcr.Charges[0].CategoryName, "")
-}
-
-func TestScanCheckDomainResponseWithFee08(t *testing.T) {
- x := `
-
-
-
- Command completed successfully
-
-
-
-
- crrc.yln
-
-
-
-
-
-
- crrc.yln
- CNY
- create
- 1
- 100.00
- premium
-
-
-
-
- testnn-domain-check-f193d63b-1ab7-43bc-bc9d-4e835fb0fece
- SERVER-4aafbfa9-cd31-4e89-b585-25a753d3c69a
-
-
-`
-
- var res Response
- dcr := &res.DomainCheckResponse
-
- d := decoder(x)
- err := IgnoreEOF(scanResponse.Scan(d, &res))
- st.Expect(t, err, nil)
- st.Expect(t, len(dcr.Checks), 1)
- st.Expect(t, dcr.Checks[0].Domain, "crrc.yln")
- st.Expect(t, dcr.Checks[0].Available, true)
- st.Expect(t, dcr.Checks[0].Reason, "")
- st.Expect(t, len(dcr.Charges), 1)
- st.Expect(t, dcr.Charges[0].Domain, "crrc.yln")
- st.Expect(t, dcr.Charges[0].Category, "premium")
- st.Expect(t, dcr.Charges[0].CategoryName, "")
-}
-
-func TestScanCheckDomainResponseWithFee09(t *testing.T) {
- x := `
-
-
-
- Command completed successfully
-
-
-
-
- example.com
-
-
-
-
-
-
- example.com
- USD
- create
- 1
- 5.00
- 5.00
- premium-tier1
-
-
-
-
- ABC-12345
- 54322-XYZ
-
-
-`
-
- var res Response
- dcr := &res.DomainCheckResponse
-
- d := decoder(x)
- err := IgnoreEOF(scanResponse.Scan(d, &res))
- st.Expect(t, err, nil)
- st.Expect(t, len(dcr.Checks), 1)
- st.Expect(t, dcr.Checks[0].Domain, "example.com")
- st.Expect(t, dcr.Checks[0].Available, true)
- st.Expect(t, dcr.Checks[0].Reason, "")
- st.Expect(t, len(dcr.Charges), 1)
- st.Expect(t, dcr.Charges[0].Domain, "example.com")
- st.Expect(t, dcr.Charges[0].Category, "premium")
- st.Expect(t, dcr.Charges[0].CategoryName, "")
-}
-
-func TestScanCheckDomainResponseWithFee11(t *testing.T) {
- x := `
-
-
-
- Command completed successfully
-
-
-
-
- example.com
-
-
-
-
-
-
- example.com
- USD
- create
- 1
- 50.00
- premium
-
-
-
-
- ABC-12345
- 54322-XYZ
-
-
-`
-
- var res Response
- dcr := &res.DomainCheckResponse
-
- d := decoder(x)
- err := IgnoreEOF(scanResponse.Scan(d, &res))
- st.Expect(t, err, nil)
- st.Expect(t, len(dcr.Checks), 1)
- st.Expect(t, dcr.Checks[0].Domain, "example.com")
- st.Expect(t, dcr.Checks[0].Available, true)
- st.Expect(t, dcr.Checks[0].Reason, "")
- st.Expect(t, len(dcr.Charges), 1)
- st.Expect(t, dcr.Charges[0].Domain, "example.com")
- st.Expect(t, dcr.Charges[0].Category, "premium")
- st.Expect(t, dcr.Charges[0].CategoryName, "")
-}
-
-func TestScanCheckDomainResponseWithFee21(t *testing.T) {
- x := `
-
-
-
- Command completed successfully
-
-
-
-
- example.com
-
-
-
-
-
- EUR
-
- example.com
-
- 1
- 25.00
-
-
- the requested launch phase is not suitable for the domain
-
-
-
-
-
- 1501792511080-81912
-
-
-`
-
- var res Response
- dcr := &res.DomainCheckResponse
-
- d := decoder(x)
- err := IgnoreEOF(scanResponse.Scan(d, &res))
- st.Expect(t, err, nil)
- st.Expect(t, len(dcr.Checks), 1)
- st.Expect(t, dcr.Checks[0].Domain, "example.com")
- st.Expect(t, dcr.Checks[0].Available, true)
- st.Expect(t, dcr.Checks[0].Reason, "")
- st.Expect(t, len(dcr.Charges), 1)
- st.Expect(t, dcr.Charges[0].Domain, "example.com")
- st.Expect(t, dcr.Charges[0].Category, "open")
- st.Expect(t, dcr.Charges[0].CategoryName, "")
-}
-
-func TestScanCheckDomainResponseWithFee21Premium(t *testing.T) {
- x := `
-
-
-
- Command completed successfully
-
-
-
-
- example.com
- not registrable in this phase
-
-
-
-
-
- EUR
-
- example.com
-
- the requested launch phase is not suitable for the domain
-
-
- 1
- 800.00
-
-
-
-
-
- 1501792511080-81912
-
-
-`
-
- var res Response
- dcr := &res.DomainCheckResponse
-
- d := decoder(x)
- err := IgnoreEOF(scanResponse.Scan(d, &res))
- st.Expect(t, err, nil)
- st.Expect(t, len(dcr.Checks), 1)
- st.Expect(t, dcr.Checks[0].Domain, "example.com")
- st.Expect(t, dcr.Checks[0].Available, false)
- st.Expect(t, dcr.Checks[0].Reason, "not registrable in this phase")
- st.Expect(t, len(dcr.Charges), 1)
- st.Expect(t, dcr.Charges[0].Domain, "example.com")
- st.Expect(t, dcr.Charges[0].Category, "custom")
- st.Expect(t, dcr.Charges[0].CategoryName, "open-1000")
-}
-
-func TestScanCheckDomainResponseWithFee10(t *testing.T) {
- x := `
-
-
-
- Command completed successfully
-
-
-
-
- example.sport
-
-
-
-
-
- USD
-
- example.sport
- standard
-
- 1
- 300.00
-
-
-
-
-
- 1612577898803-269276
-
-
- `
-
- var res Response
- dcr := &res.DomainCheckResponse
-
- d := decoder(x)
- err := IgnoreEOF(scanResponse.Scan(d, &res))
- st.Expect(t, err, nil)
- st.Expect(t, len(dcr.Checks), 1)
- st.Expect(t, dcr.Checks[0].Domain, "example.sport")
- st.Expect(t, dcr.Checks[0].Available, true)
- st.Expect(t, dcr.Checks[0].Reason, "")
-}
-
-func TestScanCheckDomainResponseWithPremiumAttribute(t *testing.T) {
- x := `
-
-
-
- Command completed successfully
-
-
-
-
- zero.work
-
-
-
-
-
-
- zero.work
- USD
- create
- 1
- 500.000
-
-
-
-
- 14470834306141
-
-
-`
-
- var res Response
- dcr := &res.DomainCheckResponse
-
- d := decoder(x)
- err := IgnoreEOF(scanResponse.Scan(d, &res))
- st.Expect(t, err, nil)
- st.Expect(t, len(dcr.Checks), 1)
- st.Expect(t, dcr.Checks[0].Domain, "zero.work")
- st.Expect(t, dcr.Checks[0].Available, true)
- st.Expect(t, dcr.Checks[0].Reason, "")
- st.Expect(t, len(dcr.Charges), 1)
- st.Expect(t, dcr.Charges[0].Domain, "zero.work")
- st.Expect(t, dcr.Charges[0].Category, "premium")
- st.Expect(t, dcr.Charges[0].CategoryName, "Registration Fee")
-}
-
-func TestScanCheckDomainResponseNeulevelExtension(t *testing.T) {
- x := `
-
-
-
- Command completed successfully
-
-
-
-
- 420.earth
-
-
-
-
-
- TierName=EARTH_Tier3 AnnualTierPrice=120.00
-
-
-
- 0000000000000002
- 83fa5767-5624-4be5-9e54-0b3a52f9de5b:1
-
-
-`
-
- var res Response
- dcr := &res.DomainCheckResponse
-
- d := decoder(x)
- err := IgnoreEOF(scanResponse.Scan(d, &res))
- st.Expect(t, err, nil)
- st.Expect(t, len(dcr.Checks), 1)
- st.Expect(t, dcr.Checks[0].Domain, "420.earth")
- st.Expect(t, dcr.Checks[0].Available, true)
- st.Expect(t, dcr.Checks[0].Reason, "")
- st.Expect(t, len(dcr.Charges), 1)
- st.Expect(t, dcr.Charges[0].Domain, "420.earth")
- st.Expect(t, dcr.Charges[0].Category, "EARTH_Tier3")
- st.Expect(t, dcr.Charges[0].CategoryName, "")
-}
-
-func TestScanCheckDomainResponsePriceExtension(t *testing.T) {
- x := `
-
-
-
- Command completed successfully
-
-
-
-
- foundations.build
- 1
- 1500
- 1500
- 40
- 1500
-
-
-
-
- aaa39bf9-12dd-4810-bdb2-98f629cfbbbb
-
-
-`
-
- var res Response
- dcr := &res.DomainCheckResponse
-
- d := decoder(x)
- err := IgnoreEOF(scanResponse.Scan(d, &res))
- st.Expect(t, err, nil)
- st.Expect(t, len(dcr.Checks), 0)
- st.Expect(t, len(dcr.Charges), 1)
- st.Expect(t, dcr.Charges[0].Domain, "foundations.build")
- st.Expect(t, dcr.Charges[0].Category, "premium")
- st.Expect(t, dcr.Charges[0].CategoryName, "")
-}
-
-func BenchmarkEncodeDomainCheck(b *testing.B) {
- domains := []string{"hello.com"}
- for i := 0; i < b.N; i++ {
- encodeDomainCheck(nil, domains, nil)
- }
-}
-
-func BenchmarkScanDomainCheckResponse(b *testing.B) {
- x := `
-
-
-
- Command completed successfully
-
-
-
-
- good.memorial
- premium name
-
-
-
-
-
-
- good.memorial
-
- premium
- price
- 100.00
- 100.00
- 100.00
- 50.00
-
-
-
-
-
- 0000000000000002
- 83fa5767-5624-4be5-9e54-0b3a52f9de5b:1
-
-
-`
-
- for i := 0; i < b.N; i++ {
- b.StopTimer()
- d := decoder(x)
- b.StartTimer()
- var res Response
- scanResponse.Scan(d, &res)
- }
-}
diff --git a/cmd/epp/epp.go b/cmd/epp/epp.go
deleted file mode 100644
index a698856..0000000
--- a/cmd/epp/epp.go
+++ /dev/null
@@ -1,200 +0,0 @@
-package main
-
-import (
- "crypto/tls"
- "crypto/x509"
- "flag"
- "fmt"
- "io/ioutil"
- "net"
- "net/url"
- "os"
- "time"
-
- "golang.org/x/net/proxy"
-
- "github.com/domainr/epp"
- "github.com/wsxiaoys/terminal/color"
-)
-
-func main() {
- var uri, addr, user, pass, proxyAddr, crtPath, caPath, keyPath string
- var useTLS, batch, verbose bool
-
- flag.StringVar(&uri, "url", "", "EPP server URL, e.g. epp://user:pass@api.1api.net:700")
- flag.StringVar(&addr, "addr", "", "EPP server address (HOST:PORT)")
- flag.StringVar(&user, "u", "", "EPP user name")
- flag.StringVar(&pass, "p", "", "EPP password")
- flag.BoolVar(&useTLS, "tls", true, "use TLS")
- flag.StringVar(&proxyAddr, "proxy", "", "SOCKS5 proxy address (HOST:PORT)")
- flag.StringVar(&crtPath, "cert", "", "path to SSL certificate")
- flag.StringVar(&keyPath, "key", "", "path to SSL private key")
- flag.StringVar(&caPath, "ca", "", "path to SSL certificate authority")
- flag.BoolVar(&batch, "batch", false, "check all domains in a single EPP command")
- flag.BoolVar(&verbose, "v", false, "enable verbose debug logging")
-
- flag.Usage = func() {
- fmt.Fprintf(os.Stderr, "Usage: %s [arguments] \n\nAvailable arguments:\n", os.Args[0])
- flag.PrintDefaults()
- os.Exit(1)
- }
- flag.Parse()
- if len(flag.Args()) == 0 {
- flag.Usage()
- }
-
- if verbose {
- epp.DebugLogger = os.Stderr
- }
-
- domains := make([]string, len(flag.Args()))
- for i, arg := range flag.Args() {
- domains[i] = arg // FIXME: convert unicode to Punycode?
- }
-
- // Parse URL
- if uri != "" {
- addr, user, pass = parseURL(uri)
- }
- host, _, err := net.SplitHostPort(addr)
- if err != nil {
- host = addr
- }
-
- // Set up TLS
- cfg := &tls.Config{
- InsecureSkipVerify: true,
- ServerName: host,
- }
-
- // Load certificates
- if caPath != "" {
- color.Fprintf(os.Stderr, "Loading CA certificate from %s\n", caPath)
- ca, err := ioutil.ReadFile(caPath)
- fatalif(err)
- cfg.RootCAs = x509.NewCertPool()
- cfg.RootCAs.AppendCertsFromPEM(ca)
- }
-
- if crtPath != "" && keyPath != "" {
- color.Fprintf(os.Stderr, "Loading certificate %s and key %s\n", crtPath, keyPath)
- crt, err := tls.LoadX509KeyPair(crtPath, keyPath)
- fatalif(err)
- cfg.Certificates = append(cfg.Certificates, crt)
- // cfg.BuildNameToCertificate()
- useTLS = true
- }
-
- // Use TLS?
- if !useTLS {
- cfg = nil
- }
-
- // Dial
- start := time.Now()
- var conn net.Conn
- if proxyAddr != "" {
- color.Fprintf(os.Stderr, "Connecting to %s via proxy %s\n", addr, proxyAddr)
- dialer, err := proxy.SOCKS5("tcp", proxyAddr, nil, &net.Dialer{})
- fatalif(err)
- conn, err = dialer.Dial("tcp", addr)
- } else {
- color.Fprintf(os.Stderr, "Connecting to %s\n", addr)
- conn, err = net.Dial("tcp", addr)
- }
- fatalif(err)
-
- // TLS
- if useTLS {
- color.Fprintf(os.Stderr, "Establishing TLS connection\n")
- tc := tls.Client(conn, cfg)
- err = tc.Handshake()
- fatalif(err)
- conn = tc
- }
-
- // EPP
- color.Fprintf(os.Stderr, "Performing EPP handshake\n")
- c, err := epp.NewConn(conn)
- fatalif(err)
- color.Fprintf(os.Stderr, "Logging in as %s...\n", user)
- err = c.Login(user, pass, "")
- fatalif(err)
-
- // Check
- start = time.Now()
- if batch {
- dc, err := c.CheckDomain(domains...)
- logif(err)
- printDCR(dc)
- } else {
- for _, domain := range domains {
- dc, err := c.CheckDomain(domain)
- logif(err)
- printDCR(dc)
- }
- }
- qdur := time.Since(start)
-
- color.Fprintf(os.Stderr, "@{.}Query: %s Avg: %s\n", qdur, qdur/time.Duration(len(domains)))
-}
-
-func parseURL(uri string) (addr, user, pass string) {
- u, err := url.Parse(uri)
- if err != nil {
- return
- }
- host, port, err := net.SplitHostPort(u.Host)
- if host == "" {
- host = u.Host
- }
- if port == "" {
- port = DefaultEPPPort
- }
- addr = net.JoinHostPort(host, port)
- if ui := u.User; ui != nil {
- user = ui.Username()
- pass, _ = ui.Password()
- }
- return
-}
-
-// DefaultEPPPort is the default TCP port for the EPP protocol.
-const DefaultEPPPort = "700"
-
-func logif(err error) bool {
- if err != nil {
- color.Fprintf(os.Stderr, "@{r}%s\n", err)
- return true
- }
- return false
-}
-
-func fatalif(err error) {
- if logif(err) {
- color.Fprintf(os.Stderr, "@{r}EXITING\n")
- os.Exit(1)
- }
-}
-
-func printDCR(dcr *epp.DomainCheckResponse) {
- if dcr == nil {
- return
- }
- av := make(map[string]bool)
- for _, c := range dcr.Checks {
- av[c.Domain] = c.Available
- if c.Available {
- color.Printf("@{g}%s\tavail=%t\treason=%q\n", c.Domain, c.Available, c.Reason)
- } else {
- color.Printf("@{y}%s\tavail=%t\treason=%q\n", c.Domain, c.Available, c.Reason)
- }
- }
- for _, c := range dcr.Charges {
- if av[c.Domain] {
- color.Printf("@{g}%s\tcategory=%s\tname=%q\n", c.Domain, c.Category, c.CategoryName)
- } else {
- color.Printf("@{y}%s\tcategory=%s\tname=%q\n", c.Domain, c.Category, c.CategoryName)
- }
- }
-}
diff --git a/config.go b/config.go
new file mode 100644
index 0000000..e829714
--- /dev/null
+++ b/config.go
@@ -0,0 +1,80 @@
+package epp
+
+import (
+ "github.com/domainr/epp/internal/schema/epp"
+)
+
+// Config describes an EPP client or server configuration, including
+// EPP objects and extensions used for a connection.
+type Config struct {
+ // Supported EPP version(s). Typically this should not be set
+ // by either a client or server. If nil, this will default to
+ // []string{"1.0"} (currently the only supported version).
+ Versions []string
+
+ // BCP 47 language code(s) for human-readable messages.
+ // For clients, this describes the desired language(s) in preferred order.
+ // If the server does not support any of the client’s preferred languages,
+ // the first language advertised by the server will be selected.
+ // For servers, this describes its supported language(s).
+ // If nil, []string{"en"} will be used.
+ Languages []string
+
+ // Namespace URIs of EPP objects supported by a client or server.
+ // For clients, this describes the object type(s) the client wants to access.
+ // For servers, this describes the object type(s) the server allows clients to access.
+ // If nil, a reasonable set of defaults will be used.
+ Objects []string
+
+ // EPP extension URIs supported by a client or server.
+ // For clients, this is a list of extensions(s) the client wants to use in preferred order.
+ // If nil, a client will use the highest version of each supported extension advertised by the server.
+ // For servers, this is an advertised list of supported extension(s).
+ // If nil, a server will use a reasonable set of defaults.
+ Extensions []string
+
+ // ForcedExtensions contains one or more EPP extension URIs to be used
+ // by a client or server, whether or not the peer indicates support for
+ // it. This is used as a workaround for EPP servers that incorrectly
+ // advertise the extensions they support. This value should typically be
+ // left nil. This will always be nil when read from a peer.
+ ForcedExtensions []string
+
+ // TransactionID, if not nil, returns unique values used for client or server transaction IDs.
+ // If nil, a sequential transaction ID with a random prefix will be used.
+ // The function must be safe to call from multiple goroutines.
+ TransactionID func() string
+}
+
+func configFromGreeting(g *epp.Greeting) Config {
+ c := Config{}
+ // TODO: should epp.Greeting have getter and setter methods to access deeply-nested data?
+ if g.ServiceMenu != nil {
+ c.Versions = copySlice(g.ServiceMenu.Versions)
+ c.Languages = copySlice(g.ServiceMenu.Languages)
+ c.Objects = copySlice(g.ServiceMenu.Objects)
+ if g.ServiceMenu.ServiceExtension != nil {
+ c.Extensions = copySlice(g.ServiceMenu.ServiceExtension.Extensions)
+ }
+ }
+ return c
+}
+
+// Copy deep copy of c.
+func (c Config) Copy() Config {
+ c.Versions = copySlice(c.Versions)
+ c.Languages = copySlice(c.Languages)
+ c.Objects = copySlice(c.Objects)
+ c.Extensions = copySlice(c.Extensions)
+ c.ForcedExtensions = copySlice(c.ForcedExtensions)
+ return c
+}
+
+func copySlice(s []string) []string {
+ if s == nil {
+ return nil
+ }
+ dst := make([]string, len(s))
+ copy(dst, s)
+ return dst
+}
diff --git a/conn.go b/conn.go
index 5dc5daf..503dab7 100644
--- a/conn.go
+++ b/conn.go
@@ -2,158 +2,151 @@ package epp
import (
"encoding/binary"
- "encoding/xml"
"io"
"net"
"sync"
- "time"
)
-// IgnoreEOF returns err unless err == io.EOF,
-// in which case it returns nil.
-func IgnoreEOF(err error) error {
- if err == io.EOF {
- return nil
- }
- return err
-}
+// Conn is a generic connection that can read and write EPP data units.
+// Multiple goroutines may invoke methods on a Conn simultaneously.
+type Conn interface {
+ // ReadDataUnit reads a single EPP data unit, returning the payload bytes or an error.
+ ReadDataUnit() ([]byte, error)
-// Conn represents a single connection to an EPP server.
-// Reads and writes are serialized, so it is safe for concurrent use.
-type Conn struct {
- // Conn is the underlying net.Conn (usually a TLS connection).
- net.Conn
+ // WriteDataUnit writes a single EPP data unit, returning any error.
+ WriteDataUnit([]byte) error
- // Timeout defines the timeout for network operations.
- // It must be set at initialization. Changing it after
- // a connection is already opened will have no effect.
- Timeout time.Duration
+ // Close closes the connection.
+ Close() error
- // m protects Greeting and LoginResult.
- m sync.Mutex
+ // LocalAddr returns the local network address, if any.
+ LocalAddr() net.Addr
- // Greeting holds the last received greeting message from the server,
- // indicating server name, status, data policy and capabilities.
- //
- // Deprecated: This field is written to upon opening a new EPP connection and should not be modified.
- Greeting
+ // RemoteAddr returns the remote network address, if any.
+ RemoteAddr() net.Addr
+}
- // LoginResult holds the last received login response message's Result
- // from the server, in which some servers might include diagnostics such
- // as connection count limits.
- //
- // Deprecated: this field is written to by the Login method but otherwise is not used by this package.
- LoginResult Result
+// Pipe implements Conn using an io.Reader and an io.Writer.
+type Pipe struct {
+ // R is from by ReadDataUnit.
+ R io.Reader
- // mRead synchronizes connection reads.
- mRead sync.Mutex
+ // W is written to by WriteDataUnit.
+ W io.Writer
- // mWrite synchronizes connection writes.
- mWrite sync.Mutex
+ r sync.Mutex
+ w sync.Mutex
+}
+
+var _ Conn = &Pipe{}
- done chan struct{}
+// ReadDataUnit reads a single EPP data unit from t, returning the payload bytes or an error.
+func (t *Pipe) ReadDataUnit() ([]byte, error) {
+ t.r.Lock()
+ defer t.r.Unlock()
+ return ReadDataUnit(t.R)
}
-// NewConn initializes an epp.Conn from a net.Conn and performs the EPP
-// handshake. It reads and stores the initial EPP message.
-// https://tools.ietf.org/html/rfc5730#section-2.4
-func NewConn(conn net.Conn) (*Conn, error) {
- return NewTimeoutConn(conn, 0)
+// WriteDataUnit writes a single EPP data unit to t or returns an error.
+func (t *Pipe) WriteDataUnit(data []byte) error {
+ t.w.Lock()
+ defer t.w.Unlock()
+ return WriteDataUnit(t.W, data)
}
-// NewTimeoutConn initializes an epp.Conn like NewConn, limiting the duration of network
-// operations on conn using Set(Read|Write)Deadline.
-func NewTimeoutConn(conn net.Conn, timeout time.Duration) (*Conn, error) {
- c := &Conn{
- Conn: conn,
- Timeout: timeout,
- done: make(chan struct{}),
+// Close attempts to close both the underlying reader and writer.
+// It will return the first error encountered.
+func (t *Pipe) Close() error {
+ var rerr, werr error
+ if c, ok := t.R.(io.Closer); ok {
+ rerr = c.Close()
+ }
+ if r, ok := t.W.(io.Reader); ok && r == t.R {
+ return rerr
+ }
+ if c, ok := t.W.(io.Closer); ok {
+ werr = c.Close()
}
- g, err := c.readGreeting()
- if err == nil {
- c.m.Lock()
- c.Greeting = g
- c.m.Unlock()
+ if rerr != nil {
+ return rerr
}
- return c, err
+ return werr
}
-// Close sends an EPP command and closes the connection c.
-func (c *Conn) Close() error {
- select {
- case <-c.done:
- return net.ErrClosed
- default:
+// LocalAddr attempts to return the local address of p.
+// If p.R implements LocalAddr, it will be called.
+// Otherwise, LocalAddr will return nil.
+func (p *Pipe) LocalAddr() net.Addr {
+ if a, ok := p.R.(interface{ LocalAddr() net.Addr }); ok {
+ return a.LocalAddr()
}
- c.Logout()
- close(c.done)
- return c.Conn.Close()
+ return nil
}
-// writeRequest writes a single EPP request (x) for writing on c.
-// writeRequest can be called from multiple goroutines.
-func (c *Conn) writeRequest(x []byte) error {
- c.mWrite.Lock()
- defer c.mWrite.Unlock()
- if c.Timeout > 0 {
- c.Conn.SetWriteDeadline(time.Now().Add(c.Timeout))
+// RemoteAddr attempts to return the remote address of p.
+// If p.W implements RemoteAddr, it will be called.
+// Otherwise, RemoteAddr will return nil.
+func (p *Pipe) RemoteAddr() net.Addr {
+ if a, ok := p.W.(interface{ RemoteAddr() net.Addr }); ok {
+ return a.RemoteAddr()
}
- return writeDataUnit(c.Conn, x)
+ return nil
}
-// readResponse dequeues and returns a EPP response from c.
-// It returns an error if the EPP response contains an error Result.
-// readResponse can be called from multiple goroutines.
-func (c *Conn) readResponse() (*Response, error) {
- c.mRead.Lock()
- defer c.mRead.Unlock()
- if c.Timeout > 0 {
- c.Conn.SetReadDeadline(time.Now().Add(c.Timeout))
- }
- n, err := readDataUnitHeader(c.Conn)
+// NetConn implements Conn using a net.Conn.
+type NetConn struct {
+ net.Conn
+ r sync.Mutex
+ w sync.Mutex
+}
+
+var _ Conn = &NetConn{}
+
+// ReadDataUnit reads a single EPP data unit from t, returning the payload or an error.
+func (t *NetConn) ReadDataUnit() ([]byte, error) {
+ t.r.Lock()
+ defer t.r.Unlock()
+ return ReadDataUnit(t.Conn)
+}
+
+// WriteDataUnit writes a single EPP data unit to t or returns an error.
+func (t *NetConn) WriteDataUnit(p []byte) error {
+ t.w.Lock()
+ defer t.w.Unlock()
+ return WriteDataUnit(t.Conn, p)
+}
+
+// ReadDataUnit reads a single EPP data unit from r, returning the payload or an error.
+// An EPP data unit is prefixed with 32-bit header specifying the total size
+// of the data unit (message + 4 byte header), in network (big-endian) order.
+// See http://www.ietf.org/rfc/rfc4934.txt for more information.
+func ReadDataUnit(r io.Reader) ([]byte, error) {
+ var n uint32
+ err := binary.Read(r, binary.BigEndian, &n)
if err != nil {
return nil, err
}
- r := io.LimitedReader{R: c.Conn, N: int64(n)}
- res := &Response{}
- err = IgnoreEOF(scanResponse.Scan(xml.NewDecoder(&r), res))
- if err != nil {
- return res, err
- }
- if res.Result.IsError() {
- return res, &res.Result
+ // An EPP data unit size includes the 4 byte header.
+ // See https://tools.ietf.org/html/rfc5734#section-4.
+ if n < 4 {
+ return nil, io.ErrUnexpectedEOF
}
- return res, err
+ n -= 4
+ p := make([]byte, n)
+ _, err = io.ReadAtLeast(r, p, int(n))
+ return p, err
}
-// writeDataUnit writes x to w.
+// WriteDataUnit writes a single EPP data unit to w.
// Bytes written are prefixed with 32-bit header specifying the total size
// of the data unit (message + 4 byte header), in network (big-endian) order.
-// http://www.ietf.org/rfc/rfc4934.txt
-func writeDataUnit(w io.Writer, x []byte) error {
- logXML("<-- WRITE DATA UNIT -->", x)
- s := uint32(4 + len(x))
+// See http://www.ietf.org/rfc/rfc4934.txt for more information.
+func WriteDataUnit(w io.Writer, p []byte) error {
+ s := uint32(4 + len(p))
err := binary.Write(w, binary.BigEndian, s)
if err != nil {
return err
}
- _, err = w.Write(x)
+ _, err = w.Write(p)
return err
}
-
-// readDataUnitHeader reads a single EPP data unit header from r, returning the payload size or an error.
-// An EPP data unit is prefixed with 32-bit header specifying the total size
-// of the data unit (message + 4 byte header), in network (big-endian) order.
-// http://www.ietf.org/rfc/rfc4934.txt
-func readDataUnitHeader(r io.Reader) (uint32, error) {
- var n uint32
- err := binary.Read(r, binary.BigEndian, &n)
- if err != nil {
- return 0, err
- }
- if n < 4 {
- return 0, io.ErrUnexpectedEOF
- }
- // https://tools.ietf.org/html/rfc5734#section-4
- return n - 4, err
-}
diff --git a/conn_test.go b/conn_test.go
deleted file mode 100644
index c008277..0000000
--- a/conn_test.go
+++ /dev/null
@@ -1,97 +0,0 @@
-package epp
-
-import (
- "bytes"
- "net"
- "sync"
- "testing"
-
- "github.com/nbio/st"
-)
-
-type localServer struct {
- lnmu sync.RWMutex
- net.Listener
- done chan bool // signal that indicates server stopped
-}
-
-func (ls *localServer) buildup(handler func(*localServer, net.Listener)) error {
- go func() {
- handler(ls, ls.Listener)
- close(ls.done)
- }()
- return nil
-}
-
-func (ls *localServer) teardown() {
- ls.lnmu.Lock()
- defer ls.lnmu.Unlock()
- if ls.Listener != nil {
- ls.Listener.Close()
- <-ls.done
- ls.Listener = nil
- }
-}
-
-func newLocalServer() (*localServer, error) {
- ln, err := net.Listen("tcp", "127.0.0.1:0")
- if err != nil {
- return nil, err
- }
- return &localServer{Listener: ln, done: make(chan bool)}, nil
-}
-
-func TestNewConn(t *testing.T) {
- ls, err := newLocalServer()
- st.Assert(t, err, nil)
- defer ls.teardown()
- ls.buildup(func(ls *localServer, ln net.Listener) {
- conn, err := ls.Accept()
- st.Assert(t, err, nil)
- // Respond with greeting
- err = writeDataUnit(conn, []byte(testXMLGreeting))
- st.Assert(t, err, nil)
- // Read logout message
- _, err = readDataUnitHeader(conn)
- st.Assert(t, err, nil)
- // Close connection
- err = conn.Close()
- st.Assert(t, err, nil)
- })
- nc, err := net.Dial(ls.Listener.Addr().Network(), ls.Listener.Addr().String())
- st.Assert(t, err, nil)
- c, err := NewConn(nc)
- st.Assert(t, err, nil)
- st.Reject(t, c, nil)
- st.Reject(t, c.Greeting.ServerName, "")
- err = c.Close()
- st.Expect(t, err, nil)
-}
-
-func TestDeleteRange(t *testing.T) {
- v := deleteRange([]byte(``), []byte(``))
- st.Expect(t, string(v), ``)
-
- v = deleteRange([]byte(``), []byte(``), []byte(`o>`))
- st.Expect(t, string(v), ``)
-}
-
-func deleteBufferRange(buf *bytes.Buffer, pfx, sfx []byte) {
- v := deleteRange(buf.Bytes(), pfx, sfx)
- buf.Truncate(len(v))
-}
-
-func deleteRange(s, pfx, sfx []byte) []byte {
- start := bytes.Index(s, pfx)
- if start < 0 {
- return s
- }
- end := bytes.Index(s[start+len(pfx):], sfx)
- if end < 0 {
- return s
- }
- end += start + len(pfx) + len(sfx)
- size := len(s) - (end - start)
- copy(s[start:size], s[end:])
- return s[:size]
-}
diff --git a/errors.go b/errors.go
new file mode 100644
index 0000000..a26e711
--- /dev/null
+++ b/errors.go
@@ -0,0 +1,32 @@
+package epp
+
+import (
+ "errors"
+ "fmt"
+)
+
+// ErrClosedConnection indicates a read or write operation on a closed connection.
+var ErrClosedConnection = errors.New("epp: operation on closed connection")
+
+// ErrUnexpectedHello indicates an EPP message contained an unexpected element.
+var ErrUnexpectedHello = errors.New("epp: unexpected ")
+
+// ErrUnexpectedCommand indicates an EPP message contained a element.
+var ErrUnexpectedCommand = errors.New("epp: unexpected ")
+
+// ErrNoResponse indicates an EPP message did not contain an expected element.
+var ErrNoResponse = errors.New("epp: missing ")
+
+// ErrUnexpectedResponse indicates an EPP message contained an unexpected element.
+var ErrUnexpectedResponse = errors.New("epp: unexpected ")
+
+// ErrNoTransactionID indicates an EPP message did not contain an expected transaction ID.
+var ErrNoTransactionID = errors.New("epp: missing transaction ID")
+
+// TransactionIDError indicates an invalid transaction ID.
+type TransactionIDError string
+
+// Error implements the error interface.
+func (err TransactionIDError) Error() string {
+ return fmt.Sprintf("epp: invalid transaction ID: %q", string(err))
+}
diff --git a/go.mod b/go.mod
index 782802f..478c962 100644
--- a/go.mod
+++ b/go.mod
@@ -1,10 +1,14 @@
module github.com/domainr/epp
-go 1.15
+go 1.17
require (
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32
- github.com/nbio/xx v0.0.0-20171204172743-d97a23099bf2
+ github.com/nbio/xml v0.0.0-20211203233926-213e87217328
+ github.com/nbio/xx v0.0.0-20211016162247-522295b80baa
+ github.com/rickb777/date v1.16.1
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0
- golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3
+ golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9
)
+
+require github.com/rickb777/plural v1.3.0 // indirect
diff --git a/go.sum b/go.sum
index 26944a2..25c70c7 100644
--- a/go.sum
+++ b/go.sum
@@ -1,8 +1,71 @@
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
-github.com/nbio/xx v0.0.0-20171204172743-d97a23099bf2 h1:QiO0Kf91RWZPOEULlAVLTbCHZ/meDfIRHnf8Sa7NZok=
-github.com/nbio/xx v0.0.0-20171204172743-d97a23099bf2/go.mod h1:bZz+wNArPTHg7xRNUHbGCS5+IEsB+29q1RoWJhix1rY=
+github.com/nbio/xml v0.0.0-20211014203712-dccdac7ed502/go.mod h1:Gxm91uHcm2tGopMmOefUHM6koiUsWvp/4FwYIJ9jo/k=
+github.com/nbio/xml v0.0.0-20211201183319-d6ca022aaad1 h1:U95PwWJmrD1iCwWSv2ZxFtyOfAkE1aLpoO7XHvcoazM=
+github.com/nbio/xml v0.0.0-20211201183319-d6ca022aaad1/go.mod h1:Gxm91uHcm2tGopMmOefUHM6koiUsWvp/4FwYIJ9jo/k=
+github.com/nbio/xml v0.0.0-20211203233926-213e87217328 h1:r2qjrHwFZbADThD/zsB6ARdzfkVyyseidffIOPlyHkU=
+github.com/nbio/xml v0.0.0-20211203233926-213e87217328/go.mod h1:Gxm91uHcm2tGopMmOefUHM6koiUsWvp/4FwYIJ9jo/k=
+github.com/nbio/xx v0.0.0-20211016162247-522295b80baa h1:97k42e9VIFpGssByqNkbgCN2aShfcs9OC4NSzkatpEI=
+github.com/nbio/xx v0.0.0-20211016162247-522295b80baa/go.mod h1:Ot5GE0TNFpVT69k/IFalGXyinlUN9FodpiM8FbqGU8Y=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.5 h1:7n6FEkpFmfCoo2t+YYqXH0evK+a9ICQz0xcAy9dYcaQ=
+github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
+github.com/rickb777/date v1.16.1 h1:nUx7FrnRLxwj4QpbuHOz7RRcnEyFOiXnZxdC2lx0f8c=
+github.com/rickb777/date v1.16.1/go.mod h1:QwU+l0bIHSFsMQH12voxZbC531J+lM3A/ZFq5gku8F8=
+github.com/rickb777/plural v1.3.0 h1:cN3M4IcJCGiGpa92S3xJgiBQfqGDFj7J8JyObugVwAU=
+github.com/rickb777/plural v1.3.0/go.mod h1:xyHbelv4YvJE51gjMnHvk+U2e9zIysg6lTnSQK8XUYA=
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0 h1:3UeQBvD0TFrlVjOeLOBz+CPAI8dnbqNSVwUwRrkp7vQ=
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM=
-golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3 h1:czFLhve3vsQetD6JOJ8NZZvGQIXlnN3/yXxbT6/awxI=
-golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9 h1:0qxwC5n+ttVOINCBeRHO0nq9X7uy8SDsPoi5OaCdIEI=
+golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
diff --git a/greeting.go b/greeting.go
deleted file mode 100644
index 78d93ea..0000000
--- a/greeting.go
+++ /dev/null
@@ -1,141 +0,0 @@
-package epp
-
-import (
- "encoding/xml"
-
- "github.com/nbio/xx"
-)
-
-// Hello sends a command to request a from the EPP server.
-func (c *Conn) Hello() error {
- err := c.writeRequest(xmlHello)
- if err != nil {
- return err
- }
- _, err = c.readGreeting()
- return err
-}
-
-var xmlHello = []byte(xml.Header + startEPP + `` + endEPP)
-
-// Greeting is an EPP response that represents server status and capabilities.
-// https://tools.ietf.org/html/rfc5730#section-2.4
-type Greeting struct {
- ServerName string `xml:"svID"`
- Versions []string `xml:"svcMenu>version"`
- Languages []string `xml:"svcMenu>lang"`
- Objects []string `xml:"svcMenu>objURI"`
- Extensions []string `xml:"svcMenu>svcExtension>extURI,omitempty"`
-}
-
-// SupportsObject returns true if the EPP server supports
-// the object specified by uri.
-func (g *Greeting) SupportsObject(uri string) bool {
- if g == nil {
- return false
- }
- for _, v := range g.Objects {
- if v == uri {
- return true
- }
- }
- return false
-}
-
-// SupportsExtension returns true if the EPP server supports
-// the extension specified by uri.
-func (g *Greeting) SupportsExtension(uri string) bool {
- if g == nil {
- return false
- }
- for _, v := range g.Extensions {
- if v == uri {
- return true
- }
- }
- return false
-}
-
-// EPP extension URNs
-const (
- ObjDomain = "urn:ietf:params:xml:ns:domain-1.0"
- ObjHost = "urn:ietf:params:xml:ns:host-1.0"
- ObjContact = "urn:ietf:params:xml:ns:contact-1.0"
- ObjFinance = "http://www.unitedtld.com/epp/finance-1.0"
- ExtSecDNS = "urn:ietf:params:xml:ns:secDNS-1.1"
- ExtRGP = "urn:ietf:params:xml:ns:rgp-1.0"
- ExtLaunch = "urn:ietf:params:xml:ns:launch-1.0"
- ExtIDN = "urn:ietf:params:xml:ns:idn-1.0"
- ExtCharge = "http://www.unitedtld.com/epp/charge-1.0"
- ExtFee05 = "urn:ietf:params:xml:ns:fee-0.5"
- ExtFee06 = "urn:ietf:params:xml:ns:fee-0.6"
- ExtFee07 = "urn:ietf:params:xml:ns:fee-0.7"
- ExtFee08 = "urn:ietf:params:xml:ns:fee-0.8"
- ExtFee09 = "urn:ietf:params:xml:ns:fee-0.9"
- ExtFee11 = "urn:ietf:params:xml:ns:fee-0.11"
- ExtFee21 = "urn:ietf:params:xml:ns:fee-0.21"
- ExtFee10 = "urn:ietf:params:xml:ns:epp:fee-1.0"
- ExtPrice = "urn:ar:params:xml:ns:price-1.1"
- ExtNamestore = "http://www.verisign-grs.com/epp/namestoreExt-1.1"
- ExtNeulevel = "urn:ietf:params:xml:ns:neulevel"
- ExtNeulevel10 = "urn:ietf:params:xml:ns:neulevel-1.0"
-)
-
-// ExtURNNames maps short extension names to their full URN.
-var ExtURNNames = map[string]string{
- "secDNS-1.1": ExtSecDNS,
- "rgp-1.0": ExtRGP,
- "launch-1.0": ExtLaunch,
- "idn-1.0": ExtIDN,
- "charge-1.0": ExtCharge,
- "fee-0.5": ExtFee05,
- "fee-0.6": ExtFee06,
- "fee-0.7": ExtFee07,
- "fee-0.8": ExtFee08,
- "fee-0.9": ExtFee09,
- "fee-0.11": ExtFee11,
- "fee-0.21": ExtFee21,
- "fee-1.0": ExtFee10,
- "price-1.1": ExtPrice,
- "namestoreExt-1.1": ExtNamestore,
- "neulevel": ExtNeulevel,
- "neulevel-1.0": ExtNeulevel10,
-}
-
-// TODO: check if res.Greeting is not empty.
-func (c *Conn) readGreeting() (Greeting, error) {
- res, err := c.readResponse()
- if err != nil {
- return Greeting{}, err
- }
- return res.Greeting, nil
-}
-
-func init() {
- path := "epp>greeting"
- scanResponse.MustHandleCharData(path+">svID", func(c *xx.Context) error {
- res := c.Value.(*Response)
- res.Greeting.ServerName = string(c.CharData)
- return nil
- })
- scanResponse.MustHandleCharData(path+">svcMenu>version", func(c *xx.Context) error {
- res := c.Value.(*Response)
- res.Greeting.Versions = append(res.Greeting.Versions, string(c.CharData))
- return nil
- })
- scanResponse.MustHandleCharData(path+">svcMenu>lang", func(c *xx.Context) error {
- res := c.Value.(*Response)
- res.Greeting.Languages = append(res.Greeting.Languages, string(c.CharData))
- return nil
- })
- scanResponse.MustHandleCharData(path+">svcMenu>objURI", func(c *xx.Context) error {
- res := c.Value.(*Response)
- res.Greeting.Objects = append(res.Greeting.Objects, string(c.CharData))
- return nil
- })
- scanResponse.MustHandleCharData(path+">svcMenu>svcExtension>extURI", func(c *xx.Context) error {
- res := c.Value.(*Response)
- res.Greeting.Extensions = append(res.Greeting.Extensions, string(c.CharData))
- return nil
- })
-}
diff --git a/greeting_test.go b/greeting_test.go
deleted file mode 100644
index dbbcfc4..0000000
--- a/greeting_test.go
+++ /dev/null
@@ -1,109 +0,0 @@
-package epp
-
-import (
- "bytes"
- "encoding/xml"
- "net"
- "testing"
-
- "github.com/nbio/st"
-)
-
-func TestHello(t *testing.T) {
- ls, err := newLocalServer()
- st.Assert(t, err, nil)
- defer ls.teardown()
- ls.buildup(func(ls *localServer, ln net.Listener) {
- conn, err := ls.Accept()
- st.Assert(t, err, nil)
- // Respond with greeting
- err = writeDataUnit(conn, []byte(testXMLGreeting))
- st.Assert(t, err, nil)
- // Respond with greeting for
- err = writeDataUnit(conn, []byte(testXMLGreeting))
- st.Assert(t, err, nil)
- })
- nc, err := net.Dial(ls.Listener.Addr().Network(), ls.Listener.Addr().String())
- st.Assert(t, err, nil)
-
- c, err := NewConn(nc)
- st.Assert(t, err, nil)
- err = c.Hello()
- st.Expect(t, err, nil)
- st.Expect(t, c.Greeting.ServerName, "Example EPP server epp.example.com")
-}
-
-func TestGreetingSupportsObject(t *testing.T) {
- g := Greeting{}
- st.Expect(t, g.SupportsObject(ObjDomain), false)
- st.Expect(t, g.SupportsObject(ObjHost), false)
- g.Objects = testObjects
- st.Expect(t, g.SupportsObject(ObjDomain), true)
- st.Expect(t, g.SupportsObject(ObjHost), true)
-}
-
-func TestGreetingSupportsExtension(t *testing.T) {
- g := Greeting{}
- st.Expect(t, g.SupportsExtension(ExtCharge), false)
- st.Expect(t, g.SupportsExtension(ExtIDN), false)
- g.Extensions = testExtensions
- st.Expect(t, g.SupportsExtension(ExtCharge), true)
- st.Expect(t, g.SupportsExtension(ExtIDN), true)
-}
-
-func TestScanGreeting(t *testing.T) {
- d := decoder(testXMLGreeting)
- var res Response
- err := IgnoreEOF(scanResponse.Scan(d, &res))
- st.Expect(t, err, nil)
- st.Expect(t, res.Greeting.ServerName, "Example EPP server epp.example.com")
- st.Expect(t, res.Greeting.Objects[0], "urn:ietf:params:xml:ns:obj1")
- st.Expect(t, res.Greeting.Objects[1], "urn:ietf:params:xml:ns:obj2")
- st.Expect(t, res.Greeting.Objects[2], "urn:ietf:params:xml:ns:obj3")
- st.Expect(t, res.Greeting.Extensions[0], "http://custom/obj1ext-1.0")
-}
-
-func BenchmarkScanGreeting(b *testing.B) {
- b.StopTimer()
- var buf bytes.Buffer
- d := xml.NewDecoder(&buf)
- saved := *d
- b.StartTimer()
- for i := 0; i < b.N; i++ {
- b.StopTimer()
- buf.Reset()
- buf.WriteString(testXMLGreeting)
- deleteBufferRange(&buf, []byte(``), []byte(``))
- *d = saved
- b.StartTimer()
- var res Response
- scanResponse.Scan(d, &res)
- }
-}
-
-var testXMLGreeting = `
-
-
- Example EPP server epp.example.com
- 2000-06-08T22:00:00.0Z
-
- 1.0
- en
- fr
- urn:ietf:params:xml:ns:obj1
- urn:ietf:params:xml:ns:obj2
- urn:ietf:params:xml:ns:obj3
-
- http://custom/obj1ext-1.0
-
-
-
-
-
-
-
-
-
-
-
-`
diff --git a/id.go b/id.go
new file mode 100644
index 0000000..a0e3431
--- /dev/null
+++ b/id.go
@@ -0,0 +1,35 @@
+package epp
+
+import (
+ "crypto/rand"
+ "encoding/hex"
+ "strconv"
+ "sync/atomic"
+)
+
+type ID interface {
+ ID() string
+}
+
+type seqSource struct {
+ prefix string
+ n uint64
+}
+
+func newSeqSource(prefix string) (*seqSource, error) {
+ if prefix == "" {
+ var pfx [16]byte
+ _, err := rand.Read(pfx[:])
+ if err != nil {
+ return nil, err
+ }
+ prefix = hex.EncodeToString(pfx[:])
+ }
+ return &seqSource{
+ prefix: prefix,
+ }, nil
+}
+
+func (s *seqSource) ID() string {
+ return s.prefix + strconv.FormatUint(atomic.AddUint64(&s.n, 1), 10)
+}
diff --git a/info.go b/info.go
deleted file mode 100644
index d732cfd..0000000
--- a/info.go
+++ /dev/null
@@ -1,197 +0,0 @@
-package epp
-
-import (
- "bytes"
- "encoding/xml"
- "time"
-
- "github.com/nbio/xx"
-)
-
-// DomainInfo retrieves info for a domain.
-// https://tools.ietf.org/html/rfc5731#section-3.1.2
-func (c *Conn) DomainInfo(domain string, extData map[string]string) (*DomainInfoResponse, error) {
- x, err := encodeDomainInfo(&c.Greeting, domain, extData)
- if err != nil {
- return nil, err
- }
- err = c.writeRequest(x)
- if err != nil {
- return nil, err
- }
- res, err := c.readResponse()
- if err != nil {
- return nil, err
- }
- return &res.DomainInfoResponse, nil
-}
-
-func encodeDomainInfo(greeting *Greeting, domain string, extData map[string]string) ([]byte, error) {
- buf := bytes.NewBufferString(xmlCommandPrefix)
- buf.WriteString(``)
- xml.EscapeText(buf, []byte(domain))
- buf.WriteString(``)
-
- supportsNamestore := extData["namestoreExt:subProduct"] != "" && greeting.SupportsExtension(ExtNamestore)
- hasExtension := supportsNamestore
-
- if hasExtension {
- buf.WriteString(``)
- // https://www.verisign.com/assets/epp-sdk/verisign_epp-extension_namestoreext_v01.html
- if supportsNamestore {
- buf.WriteString(``)
- buf.WriteString(``)
- buf.WriteString(extData["namestoreExt:subProduct"])
- buf.WriteString(``)
- buf.WriteString(``)
- }
- buf.WriteString(``)
- }
-
- buf.WriteString(xmlCommandSuffix)
-
- return buf.Bytes(), nil
-}
-
-// DomainInfoResponse represents an EPP response for a domain info request.
-// https://tools.ietf.org/html/rfc5731#section-3.1.2
-type DomainInfoResponse struct {
- Domain string //
- ID string //
- ClID string //
- UpID string //
- CrDate time.Time //
- ExDate time.Time //
- UpDate time.Time //
- TrDate time.Time //
- Status []string //
-}
-
-func init() {
- // Default EPP check data
- path := "epp > response > resData > " + ObjDomain + " infData"
- scanResponse.MustHandleCharData(path+">name", func(c *xx.Context) error {
- dir := &c.Value.(*Response).DomainInfoResponse
- dir.Domain = string(c.CharData)
- return nil
- })
- scanResponse.MustHandleCharData(path+">roid", func(c *xx.Context) error {
- dir := &c.Value.(*Response).DomainInfoResponse
- dir.ID = string(c.CharData)
- return nil
- })
- scanResponse.MustHandleCharData(path+">clID", func(c *xx.Context) error {
- dir := &c.Value.(*Response).DomainInfoResponse
- dir.ClID = string(c.CharData)
- return nil
- })
- scanResponse.MustHandleCharData(path+">upID", func(c *xx.Context) error {
- dir := &c.Value.(*Response).DomainInfoResponse
- dir.UpID = string(c.CharData)
- return nil
- })
- scanResponse.MustHandleCharData(path+">crDate", func(c *xx.Context) error {
- dir := &c.Value.(*Response).DomainInfoResponse
- var err error
- dir.CrDate, err = time.Parse(time.RFC3339, string(c.CharData))
- return err
- })
- scanResponse.MustHandleCharData(path+">exDate", func(c *xx.Context) error {
- dir := &c.Value.(*Response).DomainInfoResponse
- var err error
- dir.ExDate, err = time.Parse(time.RFC3339, string(c.CharData))
- return err
- })
- scanResponse.MustHandleCharData(path+">upDate", func(c *xx.Context) error {
- dir := &c.Value.(*Response).DomainInfoResponse
- var err error
- dir.UpDate, err = time.Parse(time.RFC3339, string(c.CharData))
- return err
- })
- scanResponse.MustHandleCharData(path+">trDate", func(c *xx.Context) error {
- dir := &c.Value.(*Response).DomainInfoResponse
- var err error
- dir.TrDate, err = time.Parse(time.RFC3339, string(c.CharData))
- return err
- })
- scanResponse.MustHandleStartElement(path+">status", func(c *xx.Context) error {
- dir := &c.Value.(*Response).DomainInfoResponse
- dir.Status = append(dir.Status, c.Attr("", "s"))
- return nil
- })
-}
-
-//lint:ignore U1000 keeping around for reference
-func encodeVerisignDomainInfo(buf *bytes.Buffer, domain string) error {
- buf.Reset()
- buf.WriteString(xmlCommandPrefix)
- buf.WriteString(``)
- xml.EscapeText(buf, []byte(domain))
- buf.WriteString(``)
- buf.WriteString(``)
- buf.WriteString(``)
- buf.WriteString(``)
- buf.WriteString(`com`)
- buf.WriteString(``)
- buf.WriteString(``)
- buf.WriteString(``)
- buf.WriteString(xmlCommandSuffix)
- return nil
-}
-
-//lint:ignore U1000 keeping around for reference
-func encodeVerisignContactInfo(buf *bytes.Buffer, contact string) error {
- buf.Reset()
- buf.WriteString(xmlCommandPrefix)
- buf.WriteString(``)
- xml.EscapeText(buf, []byte(contact))
- buf.WriteString(``)
- buf.WriteString(``)
- buf.WriteString(``)
- buf.WriteString(``)
- buf.WriteString(`com`)
- buf.WriteString(``)
- buf.WriteString(``)
- buf.WriteString(``)
- buf.WriteString(xmlCommandSuffix)
- return nil
-}
-
-/*
-
-
-
-
-
- example.com
-
-
-
-
- TLD
-
-
- ABC-12345
-
-
-
-
-
-
-
-
-
- sh8013
-
- 2fooBAR
-
-
-
- ABC-12345
-
-
-*/
diff --git a/internal/schema/common/ns.go b/internal/schema/common/ns.go
new file mode 100644
index 0000000..b489c34
--- /dev/null
+++ b/internal/schema/common/ns.go
@@ -0,0 +1,5 @@
+package common
+
+// NS defines the IETF URN for the EPP common namespace.
+// See https://www.iana.org/assignments/xml-registry/ns/eppcom-1.0.txt.
+const NS = "urn:ietf:params:xml:ns:eppcom-1.0"
diff --git a/internal/schema/contact/ns.go b/internal/schema/contact/ns.go
new file mode 100644
index 0000000..b7ad758
--- /dev/null
+++ b/internal/schema/contact/ns.go
@@ -0,0 +1,5 @@
+package contact
+
+// Host defines the IETF URN for the EPP contact namespace.
+// See https://www.iana.org/assignments/xml-registry/ns/contact-1.0.txt.
+const NS = "urn:ietf:params:xml:ns:contact-1.0"
diff --git a/internal/schema/domain/check.go b/internal/schema/domain/check.go
new file mode 100644
index 0000000..d1d40d4
--- /dev/null
+++ b/internal/schema/domain/check.go
@@ -0,0 +1,10 @@
+package domain
+
+// Check represents an EPP command.
+// See https://www.rfc-editor.org/rfc/rfc5730.html.
+type Check struct {
+ XMLName struct{} `xml:"urn:ietf:params:xml:ns:domain-1.0 domain:check"`
+ Names []string `xml:"domain:name,omitempty"`
+}
+
+func (Check) EPPCheck() {}
diff --git a/internal/schema/domain/ns.go b/internal/schema/domain/ns.go
new file mode 100644
index 0000000..ea6b7c7
--- /dev/null
+++ b/internal/schema/domain/ns.go
@@ -0,0 +1,6 @@
+package domain
+
+// NS defines the IETF URN for the EPP domain namespace.
+// See https://www.iana.org/assignments/xml-registry/ns/domain-1.0.txt
+// and https://datatracker.ietf.org/doc/html/rfc5731.
+const NS = "urn:ietf:params:xml:ns:domain-1.0"
diff --git a/internal/schema/epp/check.go b/internal/schema/epp/check.go
new file mode 100644
index 0000000..fc83015
--- /dev/null
+++ b/internal/schema/epp/check.go
@@ -0,0 +1,40 @@
+package epp
+
+import (
+ "github.com/domainr/epp/internal/schema/domain"
+ "github.com/nbio/xml"
+)
+
+// Check represents an EPP command as defined in RFC 5730.
+// See https://www.rfc-editor.org/rfc/rfc5730.html#section-2.9.2.1.
+type Check struct {
+ XMLName struct{} `xml:"urn:ietf:params:xml:ns:epp-1.0 check"`
+ Check check
+}
+
+func (Check) eppCommand() {}
+
+// UnmarshalXML implements the xml.Unmarshaler interface.
+// It maps known EPP check commands to their corresponding Go type.
+func (c *Check) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
+ type T Check
+ var v struct {
+ DomainCheck *domain.Check
+ // TODO: HostCheck, etc.
+ *T
+ }
+ v.T = (*T)(c)
+ err := d.DecodeElement(&v, &start)
+ if err != nil {
+ return err
+ }
+ switch {
+ case v.DomainCheck != nil:
+ c.Check = v.DomainCheck
+ }
+ return nil
+}
+
+type check interface {
+ EPPCheck()
+}
diff --git a/internal/schema/epp/command.go b/internal/schema/epp/command.go
new file mode 100644
index 0000000..333d439
--- /dev/null
+++ b/internal/schema/epp/command.go
@@ -0,0 +1,68 @@
+package epp
+
+import (
+ "github.com/nbio/xml"
+)
+
+// Command represents an EPP client as defined in RFC 5730.
+// See https://www.rfc-editor.org/rfc/rfc5730.html#section-2.5.
+type Command struct {
+ XMLName struct{} `xml:"urn:ietf:params:xml:ns:epp-1.0 command"`
+ Command command
+ ClientTransactionID string `xml:"clTRID,omitempty"`
+}
+
+func (Command) eppBody() {}
+
+// UnmarshalXML implements the xml.Unmarshaler interface.
+// It maps known EPP commands to their corresponding Go type.
+func (c *Command) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
+ type T Command
+ var v struct {
+ Check *Check `xml:"check"`
+ Create *Create `xml:"create"`
+ Delete *Delete `xml:"delete"`
+ Info *Info `xml:"info"`
+ Login *Login `xml:"login"`
+ Logout *Logout `xml:"logout"`
+ Poll *Poll `xml:"poll"`
+ Renew *Renew `xml:"renew"`
+ Transfer *Transfer `xml:"transfer"`
+ Update *Update `xml:"update"`
+ *T
+ }
+ v.T = (*T)(c)
+ err := d.DecodeElement(&v, &start)
+ if err != nil {
+ return err
+ }
+ switch {
+ case v.Check != nil:
+ c.Command = v.Check
+ case v.Create != nil:
+ c.Command = v.Create
+ case v.Delete != nil:
+ c.Command = v.Delete
+ case v.Info != nil:
+ c.Command = v.Info
+ case v.Login != nil:
+ c.Command = v.Login
+ case v.Logout != nil:
+ c.Command = v.Logout
+ case v.Poll != nil:
+ c.Command = v.Poll
+ case v.Renew != nil:
+ c.Command = v.Renew
+ case v.Transfer != nil:
+ c.Command = v.Transfer
+ case v.Update != nil:
+ c.Command = v.Update
+ }
+ return nil
+}
+
+// command is a child element of EPP .
+// Concrete command types implement this interface.
+type command interface {
+ eppCommand()
+}
diff --git a/internal/schema/epp/command_test.go b/internal/schema/epp/command_test.go
new file mode 100644
index 0000000..0016ddc
--- /dev/null
+++ b/internal/schema/epp/command_test.go
@@ -0,0 +1,56 @@
+package epp_test
+
+import (
+ "testing"
+
+ "github.com/domainr/epp/internal/schema/domain"
+ "github.com/domainr/epp/internal/schema/epp"
+ "github.com/domainr/epp/internal/schema/test"
+)
+
+func TestCommandRoundTrip(t *testing.T) {
+ tests := []struct {
+ name string
+ v interface{}
+ want string
+ wantErr bool
+ }{
+ {
+ `empty `,
+ &epp.EPP{Body: &epp.Command{}},
+ ``,
+ false,
+ },
+ {
+ `empty command`,
+ &epp.EPP{
+ Body: &epp.Command{
+ Command: &epp.Check{
+ Check: &domain.Check{},
+ },
+ },
+ },
+ ``,
+ false,
+ },
+ {
+ `single command`,
+ &epp.EPP{
+ Body: &epp.Command{
+ Command: &epp.Check{
+ Check: &domain.Check{
+ Names: []string{"example.com"},
+ },
+ },
+ },
+ },
+ `example.com`,
+ false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ test.RoundTrip(t, tt.v, tt.want, tt.wantErr)
+ })
+ }
+}
diff --git a/internal/schema/epp/create.go b/internal/schema/epp/create.go
new file mode 100644
index 0000000..aa426d4
--- /dev/null
+++ b/internal/schema/epp/create.go
@@ -0,0 +1,10 @@
+package epp
+
+// Create represents an EPP command as defined in RFC 5730.
+// See https://www.rfc-editor.org/rfc/rfc5730.html#section-2.9.3.1.
+type Create struct {
+ // TODO: DomainCreate *domain.Create
+ // TODO: HostCreate *host.Create
+}
+
+func (Create) eppCommand() {}
diff --git a/internal/schema/epp/delete.go b/internal/schema/epp/delete.go
new file mode 100644
index 0000000..5ebc2fc
--- /dev/null
+++ b/internal/schema/epp/delete.go
@@ -0,0 +1,10 @@
+package epp
+
+// Delete represents an EPP command as defined in RFC 5730.
+// See https://www.rfc-editor.org/rfc/rfc5730.html#section-2.9.3.1.
+type Delete struct {
+ // TODO: DomainDelete *domain.Delete
+ // TODO: HostDelete *host.Delete
+}
+
+func (Delete) eppCommand() {}
diff --git a/internal/schema/epp/epp.go b/internal/schema/epp/epp.go
new file mode 100644
index 0000000..7e33835
--- /dev/null
+++ b/internal/schema/epp/epp.go
@@ -0,0 +1,44 @@
+package epp
+
+import (
+ "github.com/nbio/xml"
+)
+
+// EPP represents an element as defined in RFC 5730.
+// See https://www.rfc-editor.org/rfc/rfc5730.html.
+type EPP struct {
+ XMLName struct{} `xml:"urn:ietf:params:xml:ns:epp-1.0 epp"`
+
+ // Body is any valid EPP child element.
+ Body Body
+}
+
+func (e *EPP) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
+ var v struct {
+ Hello *Hello `xml:"hello"`
+ Greeting *Greeting `xml:"greeting"`
+ Command *Command `xml:"command"`
+ Response *Response `xml:"response"`
+ }
+ err := d.DecodeElement(&v, &start)
+ if err != nil {
+ return err
+ }
+ switch {
+ case v.Hello != nil:
+ e.Body = v.Hello
+ case v.Greeting != nil:
+ e.Body = v.Greeting
+ case v.Command != nil:
+ e.Body = v.Command
+ case v.Response != nil:
+ e.Body = v.Response
+ }
+ return nil
+}
+
+// Body represents a valid EPP body element:
+// , , , and .
+type Body interface {
+ eppBody()
+}
diff --git a/internal/schema/epp/epp_test.go b/internal/schema/epp/epp_test.go
new file mode 100644
index 0000000..37f4287
--- /dev/null
+++ b/internal/schema/epp/epp_test.go
@@ -0,0 +1,53 @@
+package epp_test
+
+import (
+ "testing"
+
+ "github.com/domainr/epp/internal/schema/epp"
+ "github.com/domainr/epp/internal/schema/test"
+)
+
+func TestEPPRoundTrip(t *testing.T) {
+ tests := []struct {
+ name string
+ v interface{}
+ want string
+ wantErr bool
+ }{
+ {
+ `nil`,
+ nil,
+ ``,
+ false,
+ },
+ {
+ `empty element`,
+ &epp.EPP{},
+ ``,
+ false,
+ },
+ {
+ ` with element`,
+ &epp.EPP{Body: &epp.Hello{}},
+ ``,
+ false,
+ },
+ {
+ `empty `,
+ &epp.EPP{Body: &epp.Greeting{}},
+ ``,
+ false,
+ },
+ {
+ `empty `,
+ &epp.EPP{Body: &epp.Command{}},
+ ``,
+ false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ test.RoundTrip(t, tt.v, tt.want, tt.wantErr)
+ })
+ }
+}
diff --git a/internal/schema/epp/greeting.go b/internal/schema/epp/greeting.go
new file mode 100644
index 0000000..1ff4318
--- /dev/null
+++ b/internal/schema/epp/greeting.go
@@ -0,0 +1,102 @@
+package epp
+
+import (
+ "github.com/domainr/epp/internal/schema/std"
+ "github.com/nbio/xml"
+)
+
+// Greeting represents an EPP server message as defined in RFC 5730.
+type Greeting struct {
+ XMLName struct{} `xml:"urn:ietf:params:xml:ns:epp-1.0 greeting"`
+ ServerName string `xml:"svID,omitempty"`
+ ServerDate *std.Time `xml:"svDate"`
+ ServiceMenu *ServiceMenu `xml:"svcMenu"`
+ DCP *DCP `xml:"dcp"`
+}
+
+func (Greeting) eppBody() {}
+
+// ServiceMenu represents an EPP element as defined in RFC 5730.
+type ServiceMenu struct {
+ Versions []string `xml:"version"`
+ Languages []string `xml:"lang"`
+ Objects []string `xml:"objURI"`
+ ServiceExtension *ServiceExtension `xml:"svcExtension"`
+}
+
+// DCP represents a server data collection policy as defined in RFC 5730.
+type DCP struct {
+ Access Access `xml:"access"`
+ Statements []Statement `xml:"statement"`
+ Expiry *Expiry `xml:"expiry"`
+}
+
+// Access represents an EPP server’s scope of data access as defined in RFC 5730.
+type Access struct {
+ Null std.Bool `xml:"null"`
+ All std.Bool `xml:"all"`
+ None std.Bool `xml:"none"`
+ Other std.Bool `xml:"other"`
+ Personal std.Bool `xml:"personal"`
+ PersonalAndOther std.Bool `xml:"personalAndOther"`
+}
+
+var (
+ AccessNull = Access{Null: std.True}
+ AccessAll = Access{All: std.True}
+ AccessNone = Access{None: std.True}
+ AccessOther = Access{Other: std.True}
+ AccessPersonal = Access{Personal: std.True}
+ AccessPersonalAndOther = Access{PersonalAndOther: std.True}
+)
+
+// Statement describes an EPP server’s data collection purpose, receipient(s), and retention policy.
+type Statement struct {
+ Purpose Purpose `xml:"purpose"`
+ Recipient Recipient `xml:"recipient"`
+}
+
+// Purpose represents an EPP server’s purpose for data collection.
+type Purpose struct {
+ Admin std.Bool `xml:"admin"`
+ Contact std.Bool `xml:"contact"`
+ Provisioning std.Bool `xml:"provisioning"`
+ Other std.Bool `xml:"other"`
+}
+
+var (
+ PurposeAdmin = Purpose{Admin: std.True}
+ PurposeContact = Purpose{Contact: std.True}
+ PurposeProvisioning = Purpose{Provisioning: std.True}
+ PurposeOther = Purpose{Other: std.True}
+)
+
+// Recipient represents an EPP server’s purpose for data collection.
+type Recipient struct {
+ Other std.Bool `xml:"other"`
+ Ours *Ours `xml:"ours"`
+ Public std.Bool `xml:"public"`
+ Same std.Bool `xml:"same"`
+ Unrelated std.Bool `xml:"unrelated"`
+}
+
+// Ours represents an EPP server’s description of an recipient.
+type Ours struct {
+ Recipient string `xml:"recDesc"`
+}
+
+// MarshalXML impements the xml.Marshaler interface.
+// Writes a single self-closing if v.Recipient is not set.
+func (v *Ours) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
+ if v.Recipient == "" {
+ return e.EncodeToken(xml.SelfClosingElement(start))
+ }
+ type T Ours
+ return e.EncodeElement((*T)(v), start)
+}
+
+// Expiry defines an EPP server’s data retention duration.
+type Expiry struct {
+ Absolute *std.Time `xml:"absolute"`
+ Relative *std.Duration `xml:"relative"`
+}
diff --git a/internal/schema/epp/greeting_test.go b/internal/schema/epp/greeting_test.go
new file mode 100644
index 0000000..b9c6690
--- /dev/null
+++ b/internal/schema/epp/greeting_test.go
@@ -0,0 +1,132 @@
+package epp_test
+
+import (
+ "testing"
+
+ "github.com/domainr/epp/internal/schema/contact"
+ "github.com/domainr/epp/internal/schema/domain"
+ "github.com/domainr/epp/internal/schema/epp"
+ "github.com/domainr/epp/internal/schema/host"
+ "github.com/domainr/epp/internal/schema/std"
+ "github.com/domainr/epp/internal/schema/test"
+)
+
+func TestGreetingRoundTrip(t *testing.T) {
+ tests := []struct {
+ name string
+ v interface{}
+ want string
+ wantErr bool
+ }{
+ {
+ `empty `,
+ &epp.EPP{Body: &epp.Greeting{}},
+ ``,
+ false,
+ },
+ {
+ `simple `,
+ &epp.EPP{
+ Body: &epp.Greeting{
+ ServerName: "Test EPP Server",
+ ServerDate: std.ParseTime("2000-01-01T00:00:00Z").Pointer(),
+ },
+ },
+ `Test EPP Server2000-01-01T00:00:00Z`,
+ false,
+ },
+ {
+ `complex `,
+ &epp.EPP{
+ Body: &epp.Greeting{
+ ServerName: "Test EPP Server",
+ ServerDate: std.ParseTime("2000-01-01T00:00:00Z").Pointer(),
+ ServiceMenu: &epp.ServiceMenu{
+ Versions: []string{"1.0"},
+ Languages: []string{"en", "fr"},
+ Objects: []string{contact.NS, domain.NS, host.NS},
+ },
+ DCP: &epp.DCP{},
+ },
+ },
+ `Test EPP Server2000-01-01T00:00:00Z1.0enfrurn:ietf:params:xml:ns:contact-1.0urn:ietf:params:xml:ns:domain-1.0urn:ietf:params:xml:ns:host-1.0`,
+ false,
+ },
+ {
+ `complex with complex `,
+ &epp.EPP{
+ Body: &epp.Greeting{
+ ServerName: "Test EPP Server",
+ ServerDate: std.ParseTime("2000-01-01T00:00:00Z").Pointer(),
+ ServiceMenu: &epp.ServiceMenu{
+ Versions: []string{"1.0"},
+ Languages: []string{"en", "fr"},
+ Objects: []string{contact.NS, domain.NS, host.NS},
+ },
+ DCP: &epp.DCP{
+ Access: epp.AccessPersonalAndOther,
+ Statements: []epp.Statement{
+ {
+ Purpose: epp.PurposeAdmin,
+ Recipient: epp.Recipient{Ours: &epp.Ours{Recipient: "Domainr"}, Public: std.True},
+ },
+ {
+ Purpose: epp.Purpose{Contact: std.True, Other: std.True},
+ Recipient: epp.Recipient{Other: std.True, Ours: &epp.Ours{}, Public: std.True},
+ },
+ },
+ Expiry: &epp.Expiry{
+ Relative: std.ParseDuration("P1Y").Pointer(),
+ },
+ },
+ },
+ },
+ `Test EPP Server2000-01-01T00:00:00Z1.0enfrurn:ietf:params:xml:ns:contact-1.0urn:ietf:params:xml:ns:domain-1.0urn:ietf:params:xml:ns:host-1.0DomainrP365DT5H49M12S`,
+ false,
+ },
+ {
+ ` with with absolute expiry`,
+ &epp.EPP{
+ Body: &epp.Greeting{
+ DCP: &epp.DCP{
+ Expiry: &epp.Expiry{
+ Absolute: std.ParseTime("2000-01-01T00:00:00Z").Pointer(),
+ },
+ },
+ },
+ },
+ `2000-01-01T00:00:00Z`,
+ false,
+ },
+ {
+ `complex with extensions`,
+ &epp.EPP{
+ Body: &epp.Greeting{
+ ServerName: "Test EPP Server",
+ ServerDate: std.ParseTime("2000-01-01T00:00:00Z").Pointer(),
+ ServiceMenu: &epp.ServiceMenu{
+ Versions: []string{"1.0"},
+ Languages: []string{"en", "fr"},
+ Objects: []string{contact.NS, domain.NS, host.NS},
+ ServiceExtension: &epp.ServiceExtension{
+ Extensions: []string{
+ "urn:ietf:params:xml:ns:fee-0.8",
+ "urn:ietf:params:xml:ns:epp:fee-1.0",
+ },
+ },
+ },
+ DCP: &epp.DCP{
+ Access: epp.AccessNull,
+ },
+ },
+ },
+ `Test EPP Server2000-01-01T00:00:00Z1.0enfrurn:ietf:params:xml:ns:contact-1.0urn:ietf:params:xml:ns:domain-1.0urn:ietf:params:xml:ns:host-1.0urn:ietf:params:xml:ns:fee-0.8urn:ietf:params:xml:ns:epp:fee-1.0`,
+ false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ test.RoundTrip(t, tt.v, tt.want, tt.wantErr)
+ })
+ }
+}
diff --git a/internal/schema/epp/hello.go b/internal/schema/epp/hello.go
new file mode 100644
index 0000000..277c2dd
--- /dev/null
+++ b/internal/schema/epp/hello.go
@@ -0,0 +1,7 @@
+package epp
+
+type Hello struct {
+ XMLName struct{} `xml:"urn:ietf:params:xml:ns:epp-1.0 hello,selfclosing"`
+}
+
+func (Hello) eppBody() {}
diff --git a/internal/schema/epp/info.go b/internal/schema/epp/info.go
new file mode 100644
index 0000000..cc5de48
--- /dev/null
+++ b/internal/schema/epp/info.go
@@ -0,0 +1,9 @@
+package epp
+
+// Info represents an EPP command as defined in RFC 5730.
+// See https://www.rfc-editor.org/rfc/rfc5730.html#section-2.9.2.2.
+type Info struct {
+ // TODO: DomainInfo *domain.Info
+}
+
+func (Info) eppCommand() {}
diff --git a/internal/schema/epp/login.go b/internal/schema/epp/login.go
new file mode 100644
index 0000000..a742c9c
--- /dev/null
+++ b/internal/schema/epp/login.go
@@ -0,0 +1,53 @@
+package epp
+
+// Login represents an EPP command as defined in RFC 5730.
+// See https://www.rfc-editor.org/rfc/rfc5730.html#section-2.9.1.1.
+type Login struct {
+ XMLName struct{} `xml:"urn:ietf:params:xml:ns:epp-1.0 login"`
+ ClientID string `xml:"clID"`
+ Password string `xml:"pw"`
+ NewPassword *string `xml:"newPW"`
+ Options Options `xml:"options"`
+ Services Services `xml:"svcs"`
+ command
+}
+
+func (Login) eppCommand() {}
+
+// Options represent EPP login options as defined in RFC 5730.
+type Options struct {
+ Version string `xml:"version"`
+ Lang string `xml:"lang,omitempty"`
+}
+
+// Services represent EPP login services as defined in RFC 5730.
+type Services struct {
+ Objects []string `xml:"objURI,omitempty"`
+ ServiceExtension *ServiceExtension `xml:"svcExtension"`
+}
+
+/*
+
+
+
+
+ ClientX
+ foo-BAR2
+ bar-FOO2
+
+ 1.0
+ en
+
+
+ urn:ietf:params:xml:ns:obj1
+ urn:ietf:params:xml:ns:obj2
+ urn:ietf:params:xml:ns:obj3
+
+ http://custom/obj1ext-1.0
+
+
+
+ ABC-12345
+
+
+*/
diff --git a/internal/schema/epp/login_test.go b/internal/schema/epp/login_test.go
new file mode 100644
index 0000000..7760a24
--- /dev/null
+++ b/internal/schema/epp/login_test.go
@@ -0,0 +1,122 @@
+package epp_test
+
+import (
+ "testing"
+
+ "github.com/domainr/epp/internal/schema/epp"
+ "github.com/domainr/epp/internal/schema/std"
+ "github.com/domainr/epp/internal/schema/test"
+ "github.com/domainr/epp/ns"
+)
+
+func TestLoginRoundTrip(t *testing.T) {
+ tests := []struct {
+ name string
+ v interface{}
+ want string
+ wantErr bool
+ }{
+ {
+ `empty `,
+ &epp.EPP{Body: &epp.Command{Command: &epp.Login{}}},
+ ``,
+ false,
+ },
+ {
+ `simple `,
+ &epp.EPP{
+ Body: &epp.Command{
+ Command: &epp.Login{
+ ClientID: "user",
+ Password: "password",
+ },
+ },
+ },
+ `userpassword`,
+ false,
+ },
+ {
+ `specify version 1.0`,
+ &epp.EPP{
+ Body: &epp.Command{
+ Command: &epp.Login{
+ ClientID: "user",
+ Password: "password",
+ Options: epp.Options{
+ Version: epp.Version,
+ },
+ },
+ },
+ },
+ `userpassword1.0`,
+ false,
+ },
+ {
+ `specify lang=en`,
+ &epp.EPP{
+ Body: &epp.Command{
+ Command: &epp.Login{
+ ClientID: "user",
+ Password: "password",
+ Options: epp.Options{
+ Version: epp.Version,
+ Lang: "en",
+ },
+ },
+ },
+ },
+ `userpassword1.0en`,
+ false,
+ },
+ {
+ `change password`,
+ &epp.EPP{
+ Body: &epp.Command{
+ Command: &epp.Login{
+ ClientID: "user",
+ Password: "password",
+ NewPassword: std.StringPointer("newpassword"),
+ Options: epp.Options{
+ Version: epp.Version,
+ Lang: "en",
+ },
+ },
+ },
+ },
+ `userpasswordnewpassword1.0en`,
+ false,
+ },
+ {
+ `complex `,
+ &epp.EPP{
+ Body: &epp.Command{
+ Command: &epp.Login{
+ ClientID: "user",
+ NewPassword: std.StringPointer("newpassword"),
+ Options: epp.Options{
+ Version: epp.Version,
+ Lang: "en",
+ },
+ Services: epp.Services{
+ Objects: []string{ns.Domain, ns.Contact, ns.Host},
+ ServiceExtension: &epp.ServiceExtension{
+ Extensions: []string{
+ "urn:ietf:params:xml:ns:epp:fee-0.8",
+ "urn:ietf:params:xml:ns:epp:fee-1.0",
+ "urn:ietf:params:xml:ns:idn-1.0",
+ },
+ },
+ },
+ },
+ },
+ },
+ `usernewpassword1.0enurn:ietf:params:xml:ns:domain-1.0urn:ietf:params:xml:ns:contact-1.0urn:ietf:params:xml:ns:host-1.0urn:ietf:params:xml:ns:epp:fee-0.8urn:ietf:params:xml:ns:epp:fee-1.0urn:ietf:params:xml:ns:idn-1.0`,
+ false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ test.RoundTrip(t, tt.v, tt.want, tt.wantErr)
+ })
+ }
+}
diff --git a/internal/schema/epp/logout.go b/internal/schema/epp/logout.go
new file mode 100644
index 0000000..918b932
--- /dev/null
+++ b/internal/schema/epp/logout.go
@@ -0,0 +1,7 @@
+package epp
+
+type Logout struct {
+ XMLName struct{} `xml:"urn:ietf:params:xml:ns:epp-1.0 logout,selfclosing"`
+}
+
+func (Logout) eppCommand() {}
diff --git a/internal/schema/epp/message.go b/internal/schema/epp/message.go
new file mode 100644
index 0000000..23cdc95
--- /dev/null
+++ b/internal/schema/epp/message.go
@@ -0,0 +1,8 @@
+package epp
+
+// Message represents an human-readable message + optional language identifier.
+// Used in epp>response>result>msg and epp>response>result>extValue>reason.
+type Message struct {
+ Lang string `xml:"lang,attr,omitempty"`
+ Value string `xml:",chardata"`
+}
diff --git a/internal/schema/epp/message_queue.go b/internal/schema/epp/message_queue.go
new file mode 100644
index 0000000..ba89f22
--- /dev/null
+++ b/internal/schema/epp/message_queue.go
@@ -0,0 +1,46 @@
+package epp
+
+import (
+ "github.com/domainr/epp/internal/schema/std"
+ "github.com/nbio/xml"
+)
+
+// MessageQueue represents an EPP server as defined in RFC 5730.
+type MessageQueue struct {
+ // The count attribute describes the number of messages that exist in
+ // the queue.
+ Count uint64 `xml:"count,attr"`
+
+ // The id attribute is used to uniquely identify the message at the head
+ // of the queue.
+ ID string `xml:"id,attr"`
+
+ // The element contains the following OPTIONAL child elements
+ // that MUST be returned in response to a request command and
+ // MUST NOT be returned in response to any other command, including a
+ // acknowledgement.
+
+ // The element that contains the date and time that the message
+ // was enqueued.
+ Date *std.Time `xml:"qDate"`
+
+ // The element contains a human-readable message.
+ // TODO: This element MAY contain XML content for formatting purposes,
+ // but the XML content is not specified by the protocol and will thus
+ // not be processed for validity.
+ Message *Message `xml:"msg"`
+}
+
+// MarshalXML impements the xml.Marshaler interface.
+// Writes a single self-closing tag if q.Date and q.Message are not set.
+func (q *MessageQueue) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
+ type T MessageQueue
+ type W struct {
+ XMLName struct{} `xml:",selfclosing"`
+ *T
+ }
+ if q.Date == nil && q.Message == nil {
+ return e.EncodeElement(&W{T: (*T)(q)}, start)
+ }
+ return e.EncodeElement((*T)(q), start)
+}
diff --git a/internal/schema/epp/ns.go b/internal/schema/epp/ns.go
new file mode 100644
index 0000000..ed46b5b
--- /dev/null
+++ b/internal/schema/epp/ns.go
@@ -0,0 +1,5 @@
+package epp
+
+// EPP defines the IETF URN for the EPP namespace.
+// See https://www.iana.org/assignments/xml-registry/ns/epp-1.0.txt.
+const NS = "urn:ietf:params:xml:ns:epp-1.0"
diff --git a/internal/schema/epp/poll.go b/internal/schema/epp/poll.go
new file mode 100644
index 0000000..5e85770
--- /dev/null
+++ b/internal/schema/epp/poll.go
@@ -0,0 +1,8 @@
+package epp
+
+// Poll represents an EPP command as defined in RFC 5730.
+// See https://www.rfc-editor.org/rfc/rfc5730.html#section-2.9.2.3.
+type Poll struct {
+}
+
+func (Poll) eppCommand() {}
diff --git a/internal/schema/epp/renew.go b/internal/schema/epp/renew.go
new file mode 100644
index 0000000..a9e6a80
--- /dev/null
+++ b/internal/schema/epp/renew.go
@@ -0,0 +1,9 @@
+package epp
+
+// Renew represents an EPP command as defined in RFC 5730.
+// See https://www.rfc-editor.org/rfc/rfc5730.html#section-2.9.3.1.
+type Renew struct {
+ // TODO: DomainRenew *domain.Renew
+}
+
+func (Renew) eppCommand() {}
diff --git a/internal/schema/epp/response.go b/internal/schema/epp/response.go
new file mode 100644
index 0000000..5003430
--- /dev/null
+++ b/internal/schema/epp/response.go
@@ -0,0 +1,41 @@
+package epp
+
+// Response represents an EPP server as defined in RFC 5730.
+// See https://www.rfc-editor.org/rfc/rfc5730.html#section-2.6.
+type Response struct {
+ XMLName struct{} `xml:"urn:ietf:params:xml:ns:epp-1.0 response"`
+
+ // Results contain one or more results (success or failure) of an EPP command.
+ Results []Result `xml:"result,omitempty"`
+
+ // The OPTIONAL element describes messages queued for client
+ // retrieval.
+ MessageQueue *MessageQueue `xml:"msgQ"`
+
+ // The (transaction identifier) element contains a client
+ // transaction ID of the command that elicited this response and a
+ // server transaction ID that uniquely identifies this response.
+ TransactionID TransactionID `xml:"trID"`
+}
+
+func (Response) eppBody() {}
+
+// Result represents an EPP server as defined in RFC 5730.
+type Result struct {
+ Code ResultCode `xml:"code"`
+ Message Message `xml:"message"`
+ // TODO: Values
+ ExtensionValues []ExtensionValue `xml:"extValue,omitempty"`
+}
+
+// ExtensionValue represents an extension to an EPP command result.
+type ExtensionValue struct {
+ // TODO: value
+ Reason Message `xml:"reason"`
+}
+
+// TransactionID represents an EPP server as defined in RFC 5730.
+type TransactionID struct {
+ Client string `xml:"clTRID"`
+ Server string `xml:"svTRID"`
+}
diff --git a/internal/schema/epp/response_test.go b/internal/schema/epp/response_test.go
new file mode 100644
index 0000000..7333557
--- /dev/null
+++ b/internal/schema/epp/response_test.go
@@ -0,0 +1,141 @@
+package epp_test
+
+import (
+ "testing"
+
+ "github.com/domainr/epp/internal/schema/epp"
+ "github.com/domainr/epp/internal/schema/std"
+ "github.com/domainr/epp/internal/schema/test"
+)
+
+func TestResponseRoundTrip(t *testing.T) {
+ tests := []struct {
+ name string
+ v interface{}
+ want string
+ wantErr bool
+ }{
+ {
+ `empty `,
+ &epp.EPP{Body: &epp.Response{}},
+ ``,
+ false,
+ },
+ {
+ `simple code 1000`,
+ &epp.EPP{
+ Body: &epp.Response{
+ Results: []epp.Result{
+ {
+ Code: epp.Success,
+ Message: epp.Success.Message(),
+ },
+ },
+ },
+ },
+ `1000Command completed successfully`,
+ false,
+ },
+ {
+ `multiple result codes`,
+ &epp.EPP{
+ Body: &epp.Response{
+ Results: []epp.Result{
+ {
+ Code: epp.ErrParameterRange,
+ Message: epp.ErrParameterRange.Message(),
+ },
+ {
+ Code: epp.ErrParameterSyntax,
+ Message: epp.ErrParameterSyntax.Message(),
+ },
+ },
+ },
+ },
+ `2004Parameter value range error2005Parameter value syntax error`,
+ false,
+ },
+ {
+ `with extValue>reason`,
+ &epp.EPP{
+ Body: &epp.Response{
+ Results: []epp.Result{
+ {
+ Code: epp.ErrBillingFailure,
+ Message: epp.ErrBillingFailure.Message(),
+ ExtensionValues: []epp.ExtensionValue{
+ {
+ Reason: epp.Message{Lang: "en", Value: "Command exceeds available balance"},
+ },
+ },
+ },
+ },
+ },
+ },
+ `2104Billing failureCommand exceeds available balance`,
+ false,
+ },
+ {
+ `with transaction IDs`,
+ &epp.EPP{
+ Body: &epp.Response{
+ Results: []epp.Result{
+ {
+ Code: epp.Success,
+ Message: epp.Success.Message(),
+ },
+ },
+ TransactionID: epp.TransactionID{
+ Client: "12345",
+ Server: "abcde",
+ },
+ },
+ },
+ `1000Command completed successfully12345abcde`,
+ false,
+ },
+ {
+ `with basic `,
+ &epp.EPP{
+ Body: &epp.Response{
+ MessageQueue: &epp.MessageQueue{Count: 5, ID: "67890"},
+ },
+ },
+ ``,
+ false,
+ },
+ {
+ `with with date`,
+ &epp.EPP{
+ Body: &epp.Response{
+ MessageQueue: &epp.MessageQueue{
+ Count: 5,
+ ID: "67890",
+ Date: std.ParseTime("2000-01-01T00:00:00Z").Pointer(),
+ },
+ },
+ },
+ `2000-01-01T00:00:00Z`,
+ false,
+ },
+ {
+ `with full `,
+ &epp.EPP{
+ Body: &epp.Response{
+ MessageQueue: &epp.MessageQueue{
+ Count: 5,
+ ID: "67890",
+ Date: std.ParseTime("2000-01-01T00:00:00Z").Pointer(),
+ },
+ },
+ },
+ `2000-01-01T00:00:00Z`,
+ false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ test.RoundTrip(t, tt.v, tt.want, tt.wantErr)
+ })
+ }
+}
diff --git a/internal/schema/epp/result_code.go b/internal/schema/epp/result_code.go
new file mode 100644
index 0000000..71c10ba
--- /dev/null
+++ b/internal/schema/epp/result_code.go
@@ -0,0 +1,161 @@
+package epp
+
+import "fmt"
+
+// ResultCode represents a 4-digit EPP result code.
+// See https://tools.ietf.org/rfcmarkup?doc=5730#section-3.
+// A ResultCode can be used as an error value.
+// Note: only result codes >= 2000 are considered errors.
+type ResultCode uint16
+
+// Message returns a Message representation of c.
+func (c ResultCode) Message() Message {
+ return Message{Lang: "en", Value: c.String()}
+}
+
+// MarshalText implements encoding.TextMarshaler to print c as a 4-digit number.
+func (c ResultCode) MarshalText() ([]byte, error) {
+ return []byte(fmt.Sprintf("%04d", c)), nil
+}
+
+// IsError returns true if c represents an error code (>= 2000).
+func (c ResultCode) IsError() bool {
+ return c >= 2000
+}
+
+// IsFatal returns true if c represents an error code that closes the
+// connection.
+func (c ResultCode) IsFatal() bool {
+ return c >= 2500
+}
+
+// Error returns the text representation of c if c is an error, or an empty
+// string if c is a successful result code.
+func (c ResultCode) Error() string {
+ if c.IsError() {
+ return c.String()
+ }
+ return ""
+}
+
+// String returns the English text representation of c.
+func (c ResultCode) String() string {
+ switch c {
+ case Success:
+ return "Command completed successfully"
+ case SuccessPending:
+ return "Command completed successfully; action pending"
+ case SuccessNoMessages:
+ return "Command completed successfully; no messages"
+ case SuccessAck:
+ return "Command completed successfully; ack to dequeue"
+ case SuccessEnd:
+ return "Command completed successfully; ending session"
+ case ErrUnknownCommand:
+ return "Unknown command"
+ case ErrCommandSyntax:
+ return "Command syntax error"
+ case ErrCommandUse:
+ return "Command use error"
+ case ErrRequiredParameter:
+ return "Required parameter missing"
+ case ErrParameterRange:
+ return "Parameter value range error"
+ case ErrParameterSyntax:
+ return "Parameter value syntax error"
+ case ErrUnimplementedVersion:
+ return "Unimplemented protocol version"
+ case ErrUnimplementedCommand:
+ return "Unimplemented command"
+ case ErrUnimplementedOption:
+ return "Unimplemented option"
+ case ErrUnimplementedExtension:
+ return "Unimplemented extension"
+ case ErrBillingFailure:
+ return "Billing failure"
+ case ErrNotEligbleForRenewal:
+ return "Object is not eligible for renewal"
+ case ErrNotEligibleForTransfer:
+ return "Object is not eligible for transfer"
+ case ErrAuthentication:
+ return "Authentication error"
+ case ErrAuthorization:
+ return "Authorization error"
+ case ErrInvalidAuthorization:
+ return "Invalid authorization information"
+ case ErrPendingTransfer:
+ return "Object pending transfer"
+ case ErrNotPendingTransfer:
+ return "Object not pending transfer"
+ case ErrExists:
+ return "Object exists"
+ case ErrDoesNotExist:
+ return "Object does not exist"
+ case ErrStatus:
+ return "Object status prohibits operation"
+ case ErrAssociation:
+ return "Object association prohibits operation"
+ case ErrParameterPolicy:
+ return "Parameter value policy error"
+ case ErrUnimplementedObject:
+ return "Unimplemented object service"
+ case ErrDataManagementViolation:
+ return "Data management policy violation"
+ case ErrCommandFailed:
+ return "Command failed"
+ case ErrCommandFailedClosing:
+ return "Command failed; server closing connection"
+ case ErrAuthenticationClosing:
+ return "Authentication error; server closing connection"
+ case ErrSessionLimitExceeded:
+ return "Session limit exceeded; server closing connection"
+ default:
+ return fmt.Sprintf("Status code %04d", c)
+ }
+}
+
+const (
+ ResultCodeMin ResultCode = 1000
+ ResultCodeMax ResultCode = 2599
+
+ // This should match the number of known result codes below
+ KnownResultCodes = 34
+
+ // Success result codes
+ Success ResultCode = 1000
+ SuccessPending ResultCode = 1001
+ SuccessNoMessages ResultCode = 1300
+ SuccessAck ResultCode = 1301
+ SuccessEnd ResultCode = 1500
+
+ // Error result codes
+ ErrUnknownCommand ResultCode = 2000
+ ErrCommandSyntax ResultCode = 2001
+ ErrCommandUse ResultCode = 2002
+ ErrRequiredParameter ResultCode = 2003
+ ErrParameterRange ResultCode = 2004
+ ErrParameterSyntax ResultCode = 2005
+ ErrUnimplementedVersion ResultCode = 2100
+ ErrUnimplementedCommand ResultCode = 2101
+ ErrUnimplementedOption ResultCode = 2102
+ ErrUnimplementedExtension ResultCode = 2103
+ ErrBillingFailure ResultCode = 2104
+ ErrNotEligbleForRenewal ResultCode = 2105
+ ErrNotEligibleForTransfer ResultCode = 2106
+ ErrAuthentication ResultCode = 2200
+ ErrAuthorization ResultCode = 2201
+ ErrInvalidAuthorization ResultCode = 2202
+ ErrPendingTransfer ResultCode = 2300
+ ErrNotPendingTransfer ResultCode = 2301
+ ErrExists ResultCode = 2302
+ ErrDoesNotExist ResultCode = 2303
+ ErrStatus ResultCode = 2304
+ ErrAssociation ResultCode = 2305
+ ErrParameterPolicy ResultCode = 2306
+ ErrUnimplementedObject ResultCode = 2307
+ ErrDataManagementViolation ResultCode = 2308
+ ErrCommandFailed ResultCode = 2400
+ ErrCommandFailedClosing ResultCode = 2500
+ ErrAuthenticationClosing ResultCode = 2501
+ ErrSessionLimitExceeded ResultCode = 2502
+)
diff --git a/internal/schema/epp/result_code_test.go b/internal/schema/epp/result_code_test.go
new file mode 100644
index 0000000..51ba7d0
--- /dev/null
+++ b/internal/schema/epp/result_code_test.go
@@ -0,0 +1,64 @@
+package epp_test
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/domainr/epp/internal/schema/epp"
+)
+
+func TestResultCodeMessage(t *testing.T) {
+ for c := epp.ResultCodeMin; c <= epp.ResultCodeMax; c++ {
+ got := c.Message()
+ want := epp.Message{Lang: "en", Value: c.String()}
+ if got != want {
+ t.Errorf("epp.ResultCode(%04d).Message() = %v, want %v", c, got, want)
+ }
+ }
+}
+
+func TestResultCodeIsError(t *testing.T) {
+ for c := epp.ResultCodeMin; c <= epp.ResultCodeMax; c++ {
+ got := c.IsError()
+ want := c >= 2000
+ if got != want {
+ t.Errorf("epp.ResultCode(%04d).IsError() = %t, want %t", c, got, want)
+ }
+ }
+}
+
+func TestResultCodeIsFatal(t *testing.T) {
+ for c := epp.ResultCodeMin; c <= epp.ResultCodeMax; c++ {
+ got := c.IsFatal()
+ want := c >= 2500
+ if got != want {
+ t.Errorf("epp.ResultCode(%04d).IsFatal() = %t, want %t", c, got, want)
+ }
+ }
+}
+
+func TestResultCodeError(t *testing.T) {
+ for c := epp.ResultCodeMin; c <= epp.ResultCodeMax; c++ {
+ gotErr := c.Error() != ""
+ wantErr := c.IsError()
+ if gotErr != wantErr {
+ var want string
+ if wantErr {
+ want = c.String()
+ }
+ t.Errorf("epp.ResultCode(%04d).Error() = %q, want %q", c, c.Error(), want)
+ }
+ }
+}
+
+func TestResultCodeString(t *testing.T) {
+ var known int
+ for c := epp.ResultCodeMin; c <= epp.ResultCodeMax; c++ {
+ if !strings.HasPrefix(c.String(), "Status code ") {
+ known++
+ }
+ }
+ if known != epp.KnownResultCodes {
+ t.Errorf("ResultCode values with known string values: %d, want %d", known, epp.KnownResultCodes)
+ }
+}
diff --git a/internal/schema/epp/service_extension.go b/internal/schema/epp/service_extension.go
new file mode 100644
index 0000000..230df2f
--- /dev/null
+++ b/internal/schema/epp/service_extension.go
@@ -0,0 +1,7 @@
+package epp
+
+// ServiceExtension represents an EPP element as defined in RFC 5730.
+// Used in EPP and messages.
+type ServiceExtension struct {
+ Extensions []string `xml:"extURI"`
+}
diff --git a/internal/schema/epp/transfer.go b/internal/schema/epp/transfer.go
new file mode 100644
index 0000000..91a171f
--- /dev/null
+++ b/internal/schema/epp/transfer.go
@@ -0,0 +1,9 @@
+package epp
+
+// Transfer represents an EPP command as defined in RFC 5730.
+// See https://www.rfc-editor.org/rfc/rfc5730.html#section-2.9.2.4.
+type Transfer struct {
+ // TODO: DomainTransfer *domain.Transfer
+}
+
+func (Transfer) eppCommand() {}
diff --git a/internal/schema/epp/update.go b/internal/schema/epp/update.go
new file mode 100644
index 0000000..087bf43
--- /dev/null
+++ b/internal/schema/epp/update.go
@@ -0,0 +1,10 @@
+package epp
+
+// Update represents an EPP command as defined in RFC 5730.
+// See https://www.rfc-editor.org/rfc/rfc5730.html#section-2.9.3.1.
+type Update struct {
+ // TODO: DomainUpdate *domain.Update
+ // TODO: HostUpdate *host.Update
+}
+
+func (Update) eppCommand() {}
diff --git a/internal/schema/epp/version.go b/internal/schema/epp/version.go
new file mode 100644
index 0000000..fedde40
--- /dev/null
+++ b/internal/schema/epp/version.go
@@ -0,0 +1,4 @@
+package epp
+
+// This package supports EPP version 1.0.
+const Version = "1.0"
diff --git a/internal/schema/host/ns.go b/internal/schema/host/ns.go
new file mode 100644
index 0000000..a7bcbd0
--- /dev/null
+++ b/internal/schema/host/ns.go
@@ -0,0 +1,5 @@
+package host
+
+// Host defines the IETF URN for the EPP host namespace.
+// See https://www.iana.org/assignments/xml-registry/ns/host-1.0.txt.
+const NS = "urn:ietf:params:xml:ns:host-1.0"
diff --git a/internal/schema/raw/xml.go b/internal/schema/raw/xml.go
new file mode 100644
index 0000000..657b44d
--- /dev/null
+++ b/internal/schema/raw/xml.go
@@ -0,0 +1,7 @@
+package raw
+
+// XML is a container for raw XML. Useful for single, self-closing tags, e.g.:
+// .
+type XML struct {
+ Value string `xml:",innerxml"`
+}
diff --git a/bool.go b/internal/schema/std/bool.go
similarity index 86%
rename from bool.go
rename to internal/schema/std/bool.go
index 293ccd3..b62d8d1 100644
--- a/bool.go
+++ b/internal/schema/std/bool.go
@@ -1,8 +1,8 @@
-package epp
+package std
-import "encoding/xml"
+import "github.com/nbio/xml"
-// Bool represents a bool that can be serialized to XML.
+// Bool represents a boolean value that can be serialized to XML.
// True:
// False: (no tag)
type Bool bool
@@ -17,6 +17,7 @@ var (
// UnmarshalXML impements the xml.Unmarshaler interface.
// Any tag present with this type = true.
+// TODO: support false representation.
func (b *Bool) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var v struct{}
d.DecodeElement(&v, &start)
@@ -40,8 +41,7 @@ func (b *Bool) UnmarshalXMLAttr(attr *xml.Attr) error {
// Any tag present with this type = true.
func (b Bool) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
if b {
- e.EncodeToken(start)
- e.EncodeToken(xml.EndElement{Name: start.Name})
+ e.EncodeToken(xml.SelfClosingElement(start))
}
return nil
}
diff --git a/internal/schema/std/bool_test.go b/internal/schema/std/bool_test.go
new file mode 100644
index 0000000..1bb00c5
--- /dev/null
+++ b/internal/schema/std/bool_test.go
@@ -0,0 +1,84 @@
+package std
+
+import (
+ "testing"
+
+ "github.com/domainr/epp/internal/schema/test"
+)
+
+func TestBool(t *testing.T) {
+ type T1 struct {
+ XMLName struct{} `xml:"example"`
+ Fred Bool `xml:"fred"`
+ Jane Bool `xml:"jane"`
+ Susan Bool `xml:"susan"`
+ }
+
+ type T2 struct {
+ XMLName struct{} `xml:"example,selfclosing"`
+ Fred Bool `xml:"fred,attr"`
+ Jane Bool `xml:"jane,attr,omitempty"`
+ Susan Bool `xml:"susan,attr,omitempty"`
+ }
+
+ tests := []struct {
+ name string
+ v interface{}
+ want string
+ wantErr bool
+ }{
+ {
+ `nil`,
+ nil,
+ ``,
+ false,
+ },
+ {
+ `no tags`,
+ &T1{},
+ ``,
+ false,
+ },
+ {
+ `Fred`,
+ &T1{Fred: true},
+ ``,
+ false,
+ },
+ {
+ `Jane`,
+ &T1{Jane: true},
+ ``,
+ false,
+ },
+ {
+ `Fred and Susan`,
+ &T1{Fred: true, Susan: true},
+ ``,
+ false,
+ },
+ {
+ `Fred attribute`,
+ &T2{Fred: true},
+ ``,
+ false,
+ },
+ {
+ `Jane attribute`,
+ &T2{Jane: true},
+ ``,
+ false,
+ },
+ {
+ `Fred and Susan attributes`,
+ &T2{Fred: true, Susan: true},
+ ``,
+ false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ test.RoundTrip(t, tt.v, tt.want, tt.wantErr)
+ })
+ }
+}
diff --git a/internal/schema/std/duration.go b/internal/schema/std/duration.go
new file mode 100644
index 0000000..802b9e1
--- /dev/null
+++ b/internal/schema/std/duration.go
@@ -0,0 +1,38 @@
+package std
+
+import (
+ "time"
+
+ "github.com/rickb777/date/period"
+)
+
+// Duration represents W3C XML duration values.
+// See https://www.w3.org/TR/xmlschema-2/#duration and https://www.rfc-editor.org/rfc/rfc3339.html.
+type Duration struct {
+ time.Duration
+}
+
+// ParseDuration parses an RFC 3339 duration string.
+// It returns an empty value if unable to parse s.
+func ParseDuration(s string) Duration {
+ p, _ := period.Parse(s)
+ d, _ := p.Duration()
+ return Duration{d}
+}
+
+// Pointer returns a pointer to d, useful for declaring composite literals.
+func (d Duration) Pointer() *Duration {
+ return &d
+}
+
+// MarshalText implements encoding.TextMarshaler.
+func (d *Duration) MarshalText() ([]byte, error) {
+ p, _ := period.NewOf(d.Duration)
+ return p.MarshalText()
+}
+
+// UnmarshalText implements an encoding.TextUnmarshaler that ignores parsing errors.
+func (d *Duration) UnmarshalText(text []byte) error {
+ *d = ParseDuration(string(text))
+ return nil
+}
diff --git a/internal/schema/std/element.go b/internal/schema/std/element.go
new file mode 100644
index 0000000..62b509f
--- /dev/null
+++ b/internal/schema/std/element.go
@@ -0,0 +1,10 @@
+package std
+
+import "github.com/nbio/xml"
+
+// Element is a generic XML element, used for marshaling
+// other types into an XML wrapper.
+type Element struct {
+ XMLName xml.Name
+ Contents []interface{}
+}
diff --git a/internal/schema/std/string.go b/internal/schema/std/string.go
new file mode 100644
index 0000000..feb8ef9
--- /dev/null
+++ b/internal/schema/std/string.go
@@ -0,0 +1,7 @@
+package std
+
+// StringPointer returns a pointer to s.
+// Used for declaring a pointer to a string literal.
+func StringPointer(s string) *string {
+ return &s
+}
diff --git a/internal/schema/std/template.go b/internal/schema/std/template.go
new file mode 100644
index 0000000..6e23ac9
--- /dev/null
+++ b/internal/schema/std/template.go
@@ -0,0 +1,86 @@
+package std
+
+import (
+ "io"
+ "reflect"
+ "sync"
+
+ "github.com/nbio/xml"
+)
+
+// Template maps xml.Name values to Go types. This allows decoding XML into a
+// Go struct with one or more interface{} fields, which would otherwise be skipped.
+type Template struct {
+ types sync.Map
+}
+
+// Add maps name to template value v, which must be a pointer to a concrete type.
+// If v is nil or points to a nil type, Add will silently fail.
+func (d *Template) Add(name xml.Name, v interface{}) {
+ t := reflect.TypeOf(v)
+ if t == nil {
+ return
+ }
+ for t.Kind() == reflect.Ptr {
+ t = t.Elem()
+ if t == nil {
+ return
+ }
+ }
+ d.types.Store(name, t)
+}
+
+// Type returns a reflect.Type for name.
+// Returns nil if name is not mapped.
+func (t *Template) Type(name xml.Name) reflect.Type {
+ v, ok := t.types.Load(name)
+ if !ok {
+ return nil
+ }
+ return v.(reflect.Type)
+}
+
+// New returns a new instance of the type that matches xml.Name.
+// Returns nil if the name does not have a type associated with it.
+func (t *Template) New(name xml.Name) interface{} {
+ typ := t.Type(name)
+ if typ == nil {
+ return nil
+ }
+ return reflect.New(typ).Interface()
+}
+
+// DecodeElement attempts to decode the start element using its internal map of xml.Name to reflect.Type.
+// It will silently skip unknown tags and return any XML parsing errors encountered.
+func (t *Template) DecodeElement(xd *xml.Decoder, start *xml.StartElement) (interface{}, error) {
+ v := t.New(start.Name)
+ if v == nil {
+ // Silently skip unknown tags.
+ return nil, nil
+ }
+ err := xd.DecodeElement(v, start)
+ return v, err
+}
+
+// DecodeChildren attempts to decode the immediate child elements of start.
+// It only evaluates start elements and ignores unknown tags.
+func (t *Template) DecodeChildren(d *xml.Decoder, start *xml.StartElement) ([]interface{}, error) {
+ var values []interface{}
+ for {
+ tok, err := d.Token()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return values, err
+ }
+ if start, ok := tok.(xml.StartElement); ok {
+ v, err := t.DecodeElement(d, &start)
+ if err != nil {
+ return values, err
+ }
+ values = append(values, v)
+ }
+ }
+ return values, nil
+}
diff --git a/internal/schema/std/time.go b/internal/schema/std/time.go
new file mode 100644
index 0000000..f31f049
--- /dev/null
+++ b/internal/schema/std/time.go
@@ -0,0 +1,37 @@
+package std
+
+import (
+ "time"
+)
+
+// Time represents an W3C XML date-time value.
+// See https://www.w3.org/TR/xmlschema-2/#dateTime and https://www.rfc-editor.org/rfc/rfc3339.html.
+type Time struct {
+ time.Time
+}
+
+// ParseTime parses an RFC 3339 date-time string.
+// It returns an empty value if unable to parse s.
+func ParseTime(s string) Time {
+ tt, _ := time.Parse(time.RFC3339, s)
+ return Time{tt}
+}
+
+// Pointer returns a pointer to t, useful for declaring composite literals.
+func (t Time) Pointer() *Time {
+ return &t
+}
+
+// MarshalText implements encoding.TextMarshaler.
+func (t *Time) MarshalText() ([]byte, error) {
+ if t == nil {
+ return nil, nil
+ }
+ return t.Time.MarshalText()
+}
+
+// UnmarshalText implements an encoding.TextUnmarshaler that ignores parsing errors.
+func (t *Time) UnmarshalText(text []byte) error {
+ _ = t.Time.UnmarshalText(text)
+ return nil
+}
diff --git a/internal/schema/std/time_test.go b/internal/schema/std/time_test.go
new file mode 100644
index 0000000..2b04a28
--- /dev/null
+++ b/internal/schema/std/time_test.go
@@ -0,0 +1,64 @@
+package std
+
+import (
+ "testing"
+ "time"
+
+ "github.com/domainr/epp/internal/schema/test"
+)
+
+func TestTime(t *testing.T) {
+ may19, err := time.Parse(time.RFC3339, "2015-05-19T06:34:21.1Z")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ type T struct {
+ XMLName struct{} `xml:"example"`
+ Value *Time `xml:"when"`
+ Attr *Time `xml:"when,attr,omitempty"`
+ }
+
+ tests := []struct {
+ name string
+ v interface{}
+ want string
+ wantErr bool
+ }{
+ {
+ `no tags`,
+ &T{},
+ ``,
+ false,
+ },
+ {
+ `zero value chardata`,
+ &T{Value: &Time{}},
+ `0001-01-01T00:00:00Z`,
+ false,
+ },
+ {
+ `zero value attr`,
+ &T{Attr: &Time{}},
+ ``,
+ false,
+ },
+ {
+ `chardata`,
+ &T{Value: &Time{may19}},
+ `2015-05-19T06:34:21.1Z`,
+ false,
+ },
+ {
+ `attr`,
+ &T{Attr: &Time{may19}},
+ ``,
+ false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ test.RoundTrip(t, tt.v, tt.want, tt.wantErr)
+ })
+ }
+}
diff --git a/internal/schema/std/value.go b/internal/schema/std/value.go
new file mode 100644
index 0000000..f4bf028
--- /dev/null
+++ b/internal/schema/std/value.go
@@ -0,0 +1,21 @@
+package std
+
+// XMLValuer is the interface implemented by types that need to modify their
+// representation before being marshaled into or from XML.
+//
+// XMLValue returns a value that can be marshaled to or unmarshaled from XML. It
+// will be passed a single argument, which is the value being marshaled or
+// unmarshaled. If an XMLValuer is embedded in another struct, XMLValue will be
+// called with a pointer to the outer struct.
+type XMLValuer interface {
+ XMLValue(interface{}) interface{}
+}
+
+// XMLValue will return v.XMLValue(v) if v implements XMLValuer.
+// If v does not implement XMLValuer, it will return v.
+func XMLValue(v interface{}) interface{} {
+ if v, ok := v.(XMLValuer); ok {
+ return v.XMLValue(v)
+ }
+ return v
+}
diff --git a/internal/schema/test/test.go b/internal/schema/test/test.go
new file mode 100644
index 0000000..3ff5aed
--- /dev/null
+++ b/internal/schema/test/test.go
@@ -0,0 +1,70 @@
+package test
+
+import (
+ "bytes"
+ "reflect"
+ "testing"
+
+ "github.com/nbio/xml"
+)
+
+// RoundTrip validates if v marshals to want or wantErr (if set),
+// and the resulting XML unmarshals to v.
+func RoundTrip(t *testing.T, v interface{}, want string, wantErr bool) {
+ x, err := xml.Marshal(v)
+ if (err != nil) != wantErr {
+ t.Errorf("xml.Marshal() error = %v, wantErr %v", err, wantErr)
+ return
+ }
+ if string(x) != want {
+ t.Errorf("xml.Marshal()\nGot: %v\nWant: %v", string(x), want)
+ }
+
+ if v == nil {
+ return
+ }
+
+ i := reflect.New(reflect.TypeOf(v).Elem()).Interface()
+ err = xml.Unmarshal(x, i)
+ if err != nil {
+ t.Errorf("xml.Unmarshal() error = %v", err)
+ return
+ }
+ if !reflect.DeepEqual(v, v) {
+ t.Errorf("xml.Unmarshal()\nGot: %#v\nWant: %#v", i, v)
+ }
+}
+
+// RoundTripName validates if v marshals to want or wantErr (if set),
+// and the resulting XML unmarshals to v. The outer XML tag will use name, if set.
+func RoundTripName(t *testing.T, name xml.Name, v interface{}, want string, wantErr bool) {
+ var err error
+ buf := &bytes.Buffer{}
+ enc := xml.NewEncoder(buf)
+ if name == (xml.Name{}) {
+ err = enc.Encode(v)
+ } else {
+ err = enc.EncodeElement(v, xml.StartElement{Name: name})
+ }
+ if (err != nil) != wantErr {
+ t.Errorf("XML encoding error = %v, wantErr %v", err, wantErr)
+ return
+ }
+ if buf.String() != want {
+ t.Errorf("XML encoding\nGot: %v\nWant: %v", buf.String(), want)
+ }
+
+ if v == nil {
+ return
+ }
+
+ i := reflect.New(reflect.TypeOf(v).Elem()).Interface()
+ err = xml.Unmarshal(buf.Bytes(), i)
+ if err != nil {
+ t.Errorf("xml.Unmarshal() error = %v", err)
+ return
+ }
+ if !reflect.DeepEqual(v, v) {
+ t.Errorf("xml.Unmarshal()\nGot: %#v\nWant: %#v", i, v)
+ }
+}
diff --git a/logging.go b/logging.go
deleted file mode 100644
index 3b730b4..0000000
--- a/logging.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package epp
-
-import (
- "bytes"
- "encoding/xml"
- "fmt"
- "io"
-)
-
-// DebugLogger is an io.Writer. Set to enable logging of EPP message XML.
-var DebugLogger io.Writer
-
-func logXML(pfx string, p []byte) {
- if DebugLogger == nil {
- return
- }
-
- var b bytes.Buffer
- enc := xml.NewEncoder(&b)
- enc.Indent("", "\t")
-
- dec := xml.NewDecoder(bytes.NewReader(p))
- var t xml.Token
- var err error
- for {
- t, err = dec.RawToken()
- if err == io.EOF {
- err = enc.Flush()
- break
- }
- if err != nil {
- break
- }
- err = enc.EncodeToken(t)
- if err != nil {
- break
- }
- }
- if err != nil {
- fmt.Fprintf(DebugLogger, "Indentation error. Raw XML: %s\n%s\n\n", pfx, string(p))
- return
- }
-
- fmt.Fprintf(DebugLogger, "%s (pretty-printed)\n", pfx)
- io.Copy(DebugLogger, &b)
- fmt.Fprint(DebugLogger, "\n\n")
-}
diff --git a/ns/ns.go b/ns/ns.go
new file mode 100644
index 0000000..b4d0e85
--- /dev/null
+++ b/ns/ns.go
@@ -0,0 +1,55 @@
+package ns
+
+import (
+ "github.com/domainr/epp/internal/schema/common"
+ "github.com/domainr/epp/internal/schema/contact"
+ "github.com/domainr/epp/internal/schema/domain"
+ "github.com/domainr/epp/internal/schema/epp"
+ "github.com/domainr/epp/internal/schema/host"
+)
+
+const (
+ // EPP defines the IETF URN for the EPP namespace.
+ // See https://www.iana.org/assignments/xml-registry/ns/epp-1.0.txt.
+ EPP = epp.NS
+
+ // Common defines the IETF URN for the EPP common namespace.
+ // See https://www.iana.org/assignments/xml-registry/ns/eppcom-1.0.txt.
+ Common = common.NS
+
+ // Host defines the IETF URN for the EPP contact namespace.
+ // See https://www.iana.org/assignments/xml-registry/ns/contact-1.0.txt.
+ Contact = contact.NS
+
+ // Domain defines the IETF URN for the EPP domain namespace.
+ // See https://www.iana.org/assignments/xml-registry/ns/domain-1.0.txt
+ // and https://datatracker.ietf.org/doc/html/rfc5731.
+ Domain = domain.NS
+
+ // Host defines the IETF URN for the EPP host namespace.
+ // See https://www.iana.org/assignments/xml-registry/ns/host-1.0.txt.
+ Host = host.NS
+
+ // SecDNS defines the IETF URN for the EPP DNSSEC namespace.
+ // See https://datatracker.ietf.org/doc/html/rfc5910.
+ SecDNS = "urn:ietf:params:xml:ns:secDNS-1.1"
+
+ Fee05 = "urn:ietf:params:xml:ns:fee-0.5"
+ Fee06 = "urn:ietf:params:xml:ns:fee-0.6"
+ Fee07 = "urn:ietf:params:xml:ns:fee-0.7"
+ Fee08 = "urn:ietf:params:xml:ns:fee-0.8"
+ Fee09 = "urn:ietf:params:xml:ns:fee-0.9"
+ Fee10 = "urn:ietf:params:xml:ns:epp:fee-1.0"
+ Fee11 = "urn:ietf:params:xml:ns:fee-0.11"
+ Fee21 = "urn:ietf:params:xml:ns:fee-0.21"
+ IDN = "urn:ietf:params:xml:ns:idn-1.0"
+ Launch = "urn:ietf:params:xml:ns:launch-1.0"
+ Neulevel = "urn:ietf:params:xml:ns:neulevel"
+ Neulevel10 = "urn:ietf:params:xml:ns:neulevel-1.0"
+ Price = "urn:ar:params:xml:ns:price-1.1"
+ RGP = "urn:ietf:params:xml:ns:rgp-1.0"
+
+ Finance = "http://www.unitedtld.com/epp/finance-1.0"
+ Charge = "http://www.unitedtld.com/epp/charge-1.0"
+ Namestore = "http://www.verisign-grs.com/epp/namestoreExt-1.1"
+)
diff --git a/response.go b/response.go
deleted file mode 100644
index cdaea89..0000000
--- a/response.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package epp
-
-import "github.com/nbio/xx"
-
-// Response represents an EPP response.
-type Response struct {
- Result
- Greeting
- DomainCheckResponse
- DomainInfoResponse
-}
-
-var scanResponse = xx.NewScanner()
-
-func init() {
- scanResponse.MustHandleStartElement("epp", func(c *xx.Context) error {
- *c.Value.(*Response) = Response{}
- return nil
- })
-}
diff --git a/result.go b/result.go
deleted file mode 100644
index b38408f..0000000
--- a/result.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package epp
-
-import (
- "fmt"
-
- "github.com/nbio/xx"
-)
-
-// Result represents an EPP element.
-type Result struct {
- Code int `xml:"code,attr"`
- Message string `xml:"msg"`
- Reason string `xml:"extValue>reason,omitempty"`
-}
-
-// IsError determines whether an EPP status code is an error.
-// https://tools.ietf.org/html/rfc5730#section-3
-func (r *Result) IsError() bool {
- return r.Code >= 2000
-}
-
-// IsFatal determines whether an EPP status code is a fatal response,
-// and the connection should be closed.
-// https://tools.ietf.org/html/rfc5730#section-3
-func (r *Result) IsFatal() bool {
- return r.Code >= 2500
-}
-
-// Error implements the error interface.
-func (r *Result) Error() string {
- return fmt.Sprintf("EPP result code %d: %s", r.Code, r.Message)
-}
-
-func init() {
- path := "epp > response > result"
- scanResponse.MustHandleStartElement(path, func(c *xx.Context) error {
- res := c.Value.(*Response)
- res.Result.Code = c.AttrInt("", "code")
- return nil
- })
- scanResponse.MustHandleCharData(path+"> msg", func(c *xx.Context) error {
- c.Value.(*Response).Result.Message = string(c.CharData)
- return nil
- })
- scanResponse.MustHandleCharData(path+"> extValue > reason", func(c *xx.Context) error {
- c.Value.(*Response).Result.Reason = string(c.CharData)
- return nil
- })
-}
diff --git a/result_test.go b/result_test.go
deleted file mode 100644
index 39286d0..0000000
--- a/result_test.go
+++ /dev/null
@@ -1,58 +0,0 @@
-package epp
-
-import (
- "testing"
-
- "github.com/nbio/st"
-)
-
-func TestScanResult(t *testing.T) {
- var res Response
- r := &res.Result
-
- d := decoder(`Command completed successfully`)
- err := IgnoreEOF(scanResponse.Scan(d, &res))
- st.Expect(t, err, nil)
- st.Expect(t, r.Code, 1000)
- st.Expect(t, r.Message, "Command completed successfully")
- st.Expect(t, r.IsError(), false)
- st.Expect(t, r.IsFatal(), false)
-
- // Result code >= 2000 is an error.
- d = decoder(`Command syntax error`)
- err = IgnoreEOF(scanResponse.Scan(d, &res))
- st.Expect(t, err, nil)
- st.Expect(t, r.Code, 2001)
- st.Expect(t, r.Message, "Command syntax error")
- st.Expect(t, r.IsError(), true)
- st.Expect(t, r.IsFatal(), false)
-
- // Result code 2306 is a policy error.
- d = decoder(`Parameter value policy errorThe label is too short`)
- err = IgnoreEOF(scanResponse.Scan(d, &res))
- st.Expect(t, err, nil)
- st.Expect(t, r.Code, 2306)
- st.Expect(t, r.Message, "Parameter value policy error")
- st.Expect(t, r.Reason, "The label is too short")
- st.Expect(t, r.IsError(), true)
- st.Expect(t, r.IsFatal(), false)
-
- // Result code > 2500 is a fatal error.
- d = decoder(`Authentication error; server closing connection`)
- err = IgnoreEOF(scanResponse.Scan(d, &res))
- st.Expect(t, err, nil)
- st.Expect(t, r.Code, 2501)
- st.Expect(t, r.Message, "Authentication error; server closing connection")
- st.Expect(t, r.IsError(), true)
- st.Expect(t, r.IsFatal(), true)
-}
-
-func BenchmarkScanResult(b *testing.B) {
- for i := 0; i < b.N; i++ {
- b.StopTimer()
- d := decoder(`Command completed successfully`)
- b.StartTimer()
- var res Response
- scanResponse.Scan(d, &res)
- }
-}
diff --git a/server.go b/server.go
new file mode 100644
index 0000000..58b68e5
--- /dev/null
+++ b/server.go
@@ -0,0 +1,38 @@
+package epp
+
+import (
+ "context"
+
+ "github.com/domainr/epp/internal/schema/epp"
+)
+
+// Server is an EPP version 1.0 server.
+type Server struct {
+ // Name is the name of this EPP server. It is sent to clients in a EPP
+ // message. If empty, a reasonable default will be used.
+ Name string
+
+ // Config describes the EPP server configuration. Configuration
+ // parameters are announced to EPP clients in an EPP message.
+ Config Config
+
+ // Handler is called in a goroutine for each incoming EPP connection.
+ // The connection will be closed when Handler returns.
+ Handler func(Session) error
+}
+
+type Session interface {
+ // Context returns the connection Context for this session. The Context
+ // will be canceled if the underlying Transport goes away or is closed.
+ Context() context.Context
+
+ // ReadCommand reads the next EPP command from the client. An error will
+ // be returned if the underlying connection is closed or an error occurs
+ // reading from the connection.
+ ReadCommand() (*epp.Command, error)
+
+ // WriteResponse sends an EPP response to the client. An error will
+ // be returned if the underlying connection is closed or an error occurs
+ // writing to the connection.
+ WriteResponse(*epp.Response) error
+}
diff --git a/session.go b/session.go
deleted file mode 100644
index 7c5f4cd..0000000
--- a/session.go
+++ /dev/null
@@ -1,90 +0,0 @@
-package epp
-
-import (
- "bytes"
- "encoding/xml"
-)
-
-// Login initializes an authenticated EPP session.
-// https://tools.ietf.org/html/rfc5730#section-2.9.1.1
-func (c *Conn) Login(user, password, newPassword string) error {
- err := c.writeLogin(user, password, newPassword)
- if err != nil {
- return err
- }
- res, err := c.readResponse()
- if err != nil {
- return nil
- }
- // We always have a .Result in our non-pointer, but it might be meaningless.
- // We might not have read anything. We think that the worst case is we
- // have the same zero values we'd get without the assignment-even-in-error-case.
- c.m.Lock()
- c.LoginResult = res.Result
- c.m.Unlock()
- return err
-}
-
-func (c *Conn) writeLogin(user, password, newPassword string) error {
- ver, lang := "1.0", "en"
- if len(c.Greeting.Versions) > 0 {
- ver = c.Greeting.Versions[0]
- }
- if len(c.Greeting.Languages) > 0 {
- lang = c.Greeting.Languages[0]
- }
- x, err := encodeLogin(user, password, newPassword, ver, lang, c.Greeting.Objects, c.Greeting.Extensions)
- if err != nil {
- return err
- }
- return c.writeRequest(x)
-}
-
-func encodeLogin(user, password, newPassword, version, language string, objects, extensions []string) ([]byte, error) {
- buf := bytes.NewBufferString(xmlCommandPrefix)
- buf.WriteString(``)
- xml.EscapeText(buf, []byte(user))
- buf.WriteString(``)
- xml.EscapeText(buf, []byte(password))
- if len(newPassword) > 0 {
- buf.WriteString(``)
- xml.EscapeText(buf, []byte(newPassword))
- buf.WriteString(``)
- } else {
- buf.WriteString(``)
- }
- xml.EscapeText(buf, []byte(version))
- buf.WriteString(``)
- xml.EscapeText(buf, []byte(language))
- buf.WriteString(``)
- for _, o := range objects {
- buf.WriteString(``)
- xml.EscapeText(buf, []byte(o))
- buf.WriteString(``)
- }
- if len(extensions) > 0 {
- buf.WriteString(``)
- for _, o := range extensions {
- buf.WriteString(``)
- xml.EscapeText(buf, []byte(o))
- buf.WriteString(``)
- }
- buf.WriteString(``)
- }
- buf.WriteString(``)
- buf.WriteString(xmlCommandSuffix)
- return buf.Bytes(), nil
-}
-
-// Logout sends a command to terminate an EPP session.
-// https://tools.ietf.org/html/rfc5730#section-2.9.1.2
-func (c *Conn) Logout() error {
- err := c.writeRequest(xmlLogout)
- if err != nil {
- return err
- }
- _, err = c.readResponse()
- return err
-}
-
-var xmlLogout = []byte(xmlCommandPrefix + `` + xmlCommandSuffix)
diff --git a/session_test.go b/session_test.go
deleted file mode 100644
index cadc8b0..0000000
--- a/session_test.go
+++ /dev/null
@@ -1,52 +0,0 @@
-package epp
-
-import (
- "encoding/xml"
- "testing"
-
- "github.com/nbio/st"
-)
-
-func TestEncodeLogin(t *testing.T) {
- x, err := encodeLogin("jane", "battery", "", "1.0", "en", nil, nil)
- st.Expect(t, err, nil)
- st.Expect(t, string(x), `
-janebattery1.0en`)
- var v struct{}
- err = xml.Unmarshal(x, &v)
- st.Expect(t, err, nil)
-}
-
-func TestEncodeLoginChangePassword(t *testing.T) {
- x, err := encodeLogin("jane", "battery", "horse", "1.0", "en", nil, nil)
- st.Expect(t, err, nil)
- st.Expect(t, string(x), `
-janebatteryhorse1.0en`)
- var v struct{}
- err = xml.Unmarshal(x, &v)
- st.Expect(t, err, nil)
-}
-
-var (
- testObjects = []string{
- ObjContact,
- ObjDomain,
- ObjFinance,
- ObjHost,
- }
- testExtensions = []string{
- ExtCharge,
- ExtFee05,
- ExtFee06,
- ExtIDN,
- ExtLaunch,
- ExtRGP,
- ExtSecDNS,
- }
-)
-
-func BenchmarkEncodeLogin(b *testing.B) {
- for i := 0; i < b.N; i++ {
- encodeLogin("jane", "battery", "horse", "1.0", "en", testObjects, testExtensions)
- }
-}
diff --git a/status.go b/status.go
deleted file mode 100644
index 364f593..0000000
--- a/status.go
+++ /dev/null
@@ -1,108 +0,0 @@
-package epp
-
-// Status represents EPP status codes as a bitfield.
-// https://www.icann.org/resources/pages/epp-status-codes-2014-06-16-en
-// https://tools.ietf.org/html/std69
-// https://tools.ietf.org/html/rfc3915
-type Status uint32
-
-// Status types, in order of priority, low to high.
-// Status are stored in a single integer as a bit field.
-const (
- StatusUnknown Status = iota
-
- // Server status codes set by a domain registry
- StatusOK Status = 1 << (iota - 1) // Standard status for a domain, meaning it has no pending operations or prohibitions.
- StatusLinked
- StatusAddPeriod // This grace period is provided after the initial registration of a domain name. If the registrar deletes the domain name during this period, the registry may provide credit to the registrar for the cost of the registration.
- StatusAutoRenewPeriod
- StatusInactive
- StatusPendingCreate
- StatusPendingDelete
- StatusPendingRenew
- StatusPendingRestore
- StatusPendingTransfer
- StatusPendingUpdate
- StatusRedemptionPeriod
- StatusRenewPeriod
- StatusServerDeleteProhibited
- StatusServerHold
- StatusServerRenewProhibited
- StatusServerTransferProhibited
- StatusServerUpdateProhibited
- StatusTransferPeriod
- StatusClientDeleteProhibited
- StatusClientHold
- StatusClientRenewProhibited
- StatusClientTransferProhibited
- StatusClientUpdateProhibited
-
- // RDAP status codes map roughly, but not exactly to EPP status codes.
- // https://tools.ietf.org/html/rfc8056#section-2
- StatusActive = StatusOK
- StatusAssociated = StatusLinked
-
- // StatusClient are status codes set by a domain registrar.
- StatusClient = StatusClientDeleteProhibited | StatusClientHold | StatusClientRenewProhibited | StatusClientTransferProhibited | StatusClientUpdateProhibited
-)
-
-// stringToStatus maps EPP and RDAP status strings to Status bits.
-var stringToStatus = map[string]Status{
- "ok": StatusOK,
- "active": StatusActive,
- "linked": StatusLinked,
- "associated": StatusAssociated,
- "add period": StatusAddPeriod,
- "addPeriod": StatusAddPeriod,
- "auto renew period": StatusAutoRenewPeriod,
- "autoRenewPeriod": StatusAutoRenewPeriod,
- "inactive": StatusInactive,
- "pending create": StatusPendingCreate,
- "pendingCreate": StatusPendingCreate,
- "pending delete": StatusPendingDelete,
- "pendingDelete": StatusPendingDelete,
- "pending renew": StatusPendingRenew,
- "pendingRenew": StatusPendingRenew,
- "pending restore": StatusPendingRestore,
- "pendingRestore": StatusPendingRestore,
- "pending transfer": StatusPendingTransfer,
- "pendingTransfer": StatusPendingTransfer,
- "pending update": StatusPendingUpdate,
- "pendingUpdate": StatusPendingUpdate,
- "redemption period": StatusRedemptionPeriod,
- "redemptionPeriod": StatusRedemptionPeriod,
- "renew period": StatusRenewPeriod,
- "renewPeriod": StatusRenewPeriod,
- "server delete prohibited": StatusServerDeleteProhibited,
- "serverDeleteProhibited": StatusServerDeleteProhibited,
- "server hold": StatusServerHold,
- "serverHold": StatusServerHold,
- "server renew prohibited": StatusServerRenewProhibited,
- "serverRenewProhibited": StatusServerRenewProhibited,
- "server transfer prohibited": StatusServerTransferProhibited,
- "serverTransferProhibited": StatusServerTransferProhibited,
- "server update prohibited": StatusServerUpdateProhibited,
- "serverUpdateProhibited": StatusServerUpdateProhibited,
- "transfer period": StatusTransferPeriod,
- "transferPeriod": StatusTransferPeriod,
- "client delete prohibited": StatusClientDeleteProhibited,
- "clientDeleteProhibited": StatusClientDeleteProhibited,
- "client hold": StatusClientHold,
- "clientHold": StatusClientHold,
- "client renew prohibited": StatusClientRenewProhibited,
- "clientRenewProhibited": StatusClientRenewProhibited,
- "client transfer prohibited": StatusClientTransferProhibited,
- "clientTransferProhibited": StatusClientTransferProhibited,
- "client update prohibited": StatusClientUpdateProhibited,
- "clientUpdateProhibited": StatusClientUpdateProhibited,
-}
-
-// ParseStatus returns a Status from one or more strings.
-// It does not attempt to validate the input or resolve conflicting status bits.
-func ParseStatus(in ...string) Status {
- var s Status
- for _, v := range in {
- s |= stringToStatus[v]
- }
- return s
-}
diff --git a/status/code.go b/status/code.go
new file mode 100644
index 0000000..eb471ea
--- /dev/null
+++ b/status/code.go
@@ -0,0 +1,117 @@
+package status
+
+// Code represents EPP status codes as a bitfield.
+// See https://tools.ietf.org/html/std69, https://tools.ietf.org/html/rfc3915,
+// and https://www.icann.org/resources/pages/epp-status-codes-2014-06-16-en.
+type Code uint32
+
+// EPP status codes, in order of priority, from low to high. Codes are stored in
+// a single integer as a bit field.
+const (
+ Unknown Code = iota
+
+ // OK is the default status code for a domain, meaning it has no pending operations or
+ // prohibitions.
+ OK Code = 1 << (iota - 1)
+ Linked
+
+ // This grace period is provided after the initial registration of a
+ // domain name. If the registrar deletes the domain name during this
+ // period, the registry may provide credit to the registrar for the cost
+ // of the registration.
+ AddPeriod
+ AutoRenewPeriod
+ Inactive
+ PendingCreate
+ PendingDelete
+ PendingRenew
+ PendingRestore
+ PendingTransfer
+ PendingUpdate
+ RedemptionPeriod
+ RenewPeriod
+ ServerDeleteProhibited
+ ServerHold
+ ServerRenewProhibited
+ ServerTransferProhibited
+ ServerUpdateProhibited
+ TransferPeriod
+ ClientDeleteProhibited
+ ClientHold
+ ClientRenewProhibited
+ ClientTransferProhibited
+ ClientUpdateProhibited
+
+ // RDAP status codes loosely map to EPP status codes.
+ // See https://tools.ietf.org/html/rfc8056#section-2.
+ Active = OK
+ Associated = Linked
+
+ // ClientCodes are status codes set by a domain registrar.
+ ClientCodes = ClientDeleteProhibited |
+ ClientHold |
+ ClientRenewProhibited |
+ ClientTransferProhibited |
+ ClientUpdateProhibited
+)
+
+// stringToCode maps EPP and RDAP status strings to Codes.
+var stringToCode = map[string]Code{
+ "ok": OK,
+ "active": Active,
+ "linked": Linked,
+ "associated": Associated,
+ "add period": AddPeriod,
+ "addPeriod": AddPeriod,
+ "auto renew period": AutoRenewPeriod,
+ "autoRenewPeriod": AutoRenewPeriod,
+ "inactive": Inactive,
+ "pending create": PendingCreate,
+ "pendingCreate": PendingCreate,
+ "pending delete": PendingDelete,
+ "pendingDelete": PendingDelete,
+ "pending renew": PendingRenew,
+ "pendingRenew": PendingRenew,
+ "pending restore": PendingRestore,
+ "pendingRestore": PendingRestore,
+ "pending transfer": PendingTransfer,
+ "pendingTransfer": PendingTransfer,
+ "pending update": PendingUpdate,
+ "pendingUpdate": PendingUpdate,
+ "redemption period": RedemptionPeriod,
+ "redemptionPeriod": RedemptionPeriod,
+ "renew period": RenewPeriod,
+ "renewPeriod": RenewPeriod,
+ "server delete prohibited": ServerDeleteProhibited,
+ "serverDeleteProhibited": ServerDeleteProhibited,
+ "server hold": ServerHold,
+ "serverHold": ServerHold,
+ "server renew prohibited": ServerRenewProhibited,
+ "serverRenewProhibited": ServerRenewProhibited,
+ "server transfer prohibited": ServerTransferProhibited,
+ "serverTransferProhibited": ServerTransferProhibited,
+ "server update prohibited": ServerUpdateProhibited,
+ "serverUpdateProhibited": ServerUpdateProhibited,
+ "transfer period": TransferPeriod,
+ "transferPeriod": TransferPeriod,
+ "client delete prohibited": ClientDeleteProhibited,
+ "clientDeleteProhibited": ClientDeleteProhibited,
+ "client hold": ClientHold,
+ "clientHold": ClientHold,
+ "client renew prohibited": ClientRenewProhibited,
+ "clientRenewProhibited": ClientRenewProhibited,
+ "client transfer prohibited": ClientTransferProhibited,
+ "clientTransferProhibited": ClientTransferProhibited,
+ "client update prohibited": ClientUpdateProhibited,
+ "clientUpdateProhibited": ClientUpdateProhibited,
+}
+
+// Parse returns a status code from one or more strings. It does not attempt to
+// validate the input or resolve conflicting status bits.
+func Parse(in ...string) Code {
+ var s Code
+ for _, v := range in {
+ s |= stringToCode[v]
+ }
+ return s
+}
diff --git a/time.go b/time.go
deleted file mode 100644
index e6dfe11..0000000
--- a/time.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package epp
-
-import (
- "encoding/xml"
- "time"
-)
-
-// Time represents EPP date-time values, serialized to XML in RFC-3339 format.
-// Because the default encoding.TextMarshaler implementation in time.Time uses
-// RFC-3339, we don’t need to create a custom marshaler for this type.
-type Time struct {
- time.Time
-}
-
-// UnmarshalXML implements a custom XML unmarshaler that ignores time parsing errors.
-// http://stackoverflow.com/a/25015260
-func (t *Time) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
- var v string
- d.DecodeElement(&v, &start)
- if tt, err := time.Parse(time.RFC3339, v); err == nil {
- *t = Time{tt}
- }
- return nil
-}
diff --git a/time_test.go b/time_test.go
deleted file mode 100644
index 18f19df..0000000
--- a/time_test.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package epp
-
-import (
- "encoding/xml"
- "testing"
- "time"
-
- "github.com/nbio/st"
-)
-
-func TestTime(t *testing.T) {
- x := []byte(`2015-05-19T06:34:21.1Z`)
- var y struct {
- XMLName struct{} `xml:"example"`
- When Time `xml:"when"`
- }
-
- err := xml.Unmarshal(x, &y)
- st.Expect(t, err, nil)
- tt, _ := time.Parse(time.RFC3339, "2015-05-19T06:34:21.1Z")
- st.Expect(t, y.When, Time{tt})
- z, err := xml.Marshal(&y)
- st.Expect(t, err, nil)
- st.Expect(t, string(z), string(x))
- text, err := y.When.MarshalText()
- st.Expect(t, err, nil)
- st.Expect(t, string(text), "2015-05-19T06:34:21.1Z")
-}
diff --git a/transaction.go b/transaction.go
new file mode 100644
index 0000000..b022adc
--- /dev/null
+++ b/transaction.go
@@ -0,0 +1,25 @@
+package epp
+
+import (
+ "context"
+
+ "github.com/domainr/epp/internal/schema/epp"
+)
+
+type transaction struct {
+ ctx context.Context
+ reply chan reply
+}
+
+func newTransaction(ctx context.Context) (transaction, context.CancelFunc) {
+ ctx, cancel := context.WithCancel(ctx)
+ return transaction{
+ ctx: ctx,
+ reply: make(chan reply),
+ }, cancel
+}
+
+type reply struct {
+ body epp.Body
+ err error
+}
diff --git a/transport.go b/transport.go
new file mode 100644
index 0000000..6c9e2fc
--- /dev/null
+++ b/transport.go
@@ -0,0 +1,377 @@
+package epp
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/nbio/xml"
+
+ "github.com/domainr/epp/internal/schema/epp"
+)
+
+// Transport is a low-level client for the Extensible Provisioning Protocol (EPP)
+// as defined in RFC 3790. See https://www.rfc-editor.org/rfc/rfc5730.html.
+// A Transport is safe to use from multiple goroutines.
+type Transport interface {
+ // Command sends an EPP command and returns an EPP response.
+ // It blocks until a response is received, ctx is canceled, or
+ // the underlying connection is closed.
+ //
+ // The EPP command must have a valid, unique transaction ID to correlate
+ // it with a response.
+ // TODO: should it assign a transaction ID if empty?
+ Command(ctx context.Context, cmd *epp.Command) (*epp.Response, error)
+
+ // Hello sends an EPP and returns the received.
+ // It blocks until a is received, ctx is canceled, or
+ // the underlying connection is closed.
+ Hello(ctx context.Context) (*epp.Greeting, error)
+
+ // Greeting returns the last recieved from the server.
+ // It blocks until the is received, ctx is canceled, or
+ // the underlying connection is closed.
+ Greeting(ctx context.Context) (*epp.Greeting, error)
+
+ // Close closes the connection.
+ Close() error
+}
+
+type transport struct {
+ // mWrite protects writes on t.
+ mWrite sync.Mutex
+ conn Conn
+
+ // greeting stores the most recently received from the server.
+ greeting atomic.Value
+
+ // hasGreeting is closed when the client receives an initial from the server.
+ hasGreeting chan struct{}
+
+ mHellos sync.Mutex
+ hellos []transaction
+
+ mCommands sync.Mutex
+ commands map[string]transaction
+
+ // done is closed when the client receives a fatal error or the connection is closed.
+ done chan struct{}
+}
+
+// NewTransport returns a new Transport using conn.
+func NewTransport(conn Conn) Transport {
+ t := newTransport(conn)
+ go t.readLoop()
+ return t
+}
+
+func newTransport(conn Conn) *transport {
+ return &transport{
+ conn: conn,
+ hasGreeting: make(chan struct{}),
+ commands: make(map[string]transaction),
+ done: make(chan struct{}),
+ }
+}
+
+// Close closes the connection.
+func (c *transport) Close() error {
+ select {
+ case <-c.done:
+ return net.ErrClosed
+ default:
+ close(c.done)
+ }
+ return c.conn.Close()
+}
+
+// ServerConfig returns the server configuration described in a message.
+// Will block until the an initial is received, or ctx is canceled.
+//
+// TODO: move this to Client.
+func (c *transport) ServerConfig(ctx context.Context) (Config, error) {
+ g, err := c.Greeting(ctx)
+ if err != nil {
+ return Config{}, err
+ }
+ return configFromGreeting(g), nil
+}
+
+// ServerName returns the most recently received server name.
+// Will block until an initial is received, or ctx is canceled.
+//
+// TODO: move this to Client.
+func (c *transport) ServerName(ctx context.Context) (string, error) {
+ g, err := c.Greeting(ctx)
+ if err != nil {
+ return "", err
+ }
+ return g.ServerName, nil
+}
+
+// ServerTime returns the most recently received timestamp from the server.
+// Will block until an initial is received, or ctx is canceled.
+//
+// TODO: move this to Client.
+// TODO: what is used for?
+func (c *transport) ServerTime(ctx context.Context) (time.Time, error) {
+ g, err := c.Greeting(ctx)
+ if err != nil {
+ return time.Time{}, err
+ }
+ return g.ServerDate.Time, nil
+}
+
+// Command sends an EPP command and returns an EPP response.
+// It blocks until a response is received, ctx is canceled, or
+// the underlying connection is closed.
+func (c *transport) Command(ctx context.Context, cmd *epp.Command) (*epp.Response, error) {
+ tx, cancel := newTransaction(ctx)
+ defer cancel()
+ c.pushCommand(cmd.ClientTransactionID, tx)
+
+ err := c.writeEPP(cmd)
+ if err != nil {
+ return nil, err
+ }
+
+ select {
+ case <-c.done:
+ return nil, ErrClosedConnection
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ case reply := <-tx.reply:
+ if r, ok := reply.body.(*epp.Response); ok {
+ return r, reply.err
+ }
+ return nil, reply.err
+ }
+}
+
+// Hello sends an EPP message to the server.
+// It will block until the next message is received or ctx is canceled.
+func (c *transport) Hello(ctx context.Context) (*epp.Greeting, error) {
+ tx, cancel := newTransaction(ctx)
+ defer cancel()
+ c.pushHello(tx)
+
+ err := c.writeEPP(&epp.Hello{})
+ if err != nil {
+ return nil, err
+ }
+
+ select {
+ case <-c.done:
+ return nil, ErrClosedConnection
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ case reply := <-tx.reply:
+ if g, ok := reply.body.(*epp.Greeting); ok {
+ return g, reply.err
+ }
+ return nil, reply.err
+ }
+}
+
+// Greeting returns the last recieved from the server.
+// It blocks until the is received, ctx is canceled, or
+// the underlying connection is closed.
+func (c *transport) Greeting(ctx context.Context) (*epp.Greeting, error) {
+ g := c.greeting.Load()
+ if g != nil {
+ return g.(*epp.Greeting), nil
+ }
+ select {
+ case <-c.done:
+ return nil, ErrClosedConnection
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ case <-c.hasGreeting:
+ return c.greeting.Load().(*epp.Greeting), nil
+ }
+}
+
+// writeEPP writes body to the underlying Transport.
+// Writes are synchronized, so it is safe to call this from multiple goroutines.
+func (c *transport) writeEPP(body epp.Body) error {
+ x, err := xml.Marshal(epp.EPP{Body: body})
+ if err != nil {
+ return err
+ }
+ return c.writeDataUnit(x)
+}
+
+// writeDataUnit writes a single EPP data unit to the underlying Transport.
+// Writes are synchronized, so it is safe to call this from multiple goroutines.
+func (c *transport) writeDataUnit(p []byte) error {
+ c.mWrite.Lock()
+ defer c.mWrite.Unlock()
+ return c.conn.WriteDataUnit(p)
+}
+
+// readLoop reads EPP messages from c.t and sends them to c.responses.
+// It closes c.responses before returning.
+// I/O errors are considered fatal and are returned.
+func (c *transport) readLoop() {
+ var err error
+ defer func() {
+ c.cleanup(err)
+ }()
+ for {
+ select {
+ case <-c.done:
+ return
+ default:
+ }
+
+ var p []byte
+ p, err = c.conn.ReadDataUnit()
+ if err != nil {
+ // TODO: log I/O errors.
+ return
+ }
+
+ err = c.handleDataUnit(p)
+ if err != nil {
+ // TODO: log XML and processing errors.
+ }
+ }
+}
+
+func (c *transport) handleDataUnit(p []byte) error {
+ var e epp.EPP
+ err := xml.Unmarshal(p, &e)
+ if err != nil {
+ // TODO: log XML parsing errors.
+ // TODO: should XML parsing errors be considered fatal?
+ return err
+ }
+
+ // TODO: log processing errors.
+ return c.handleReply(e.Body)
+}
+
+func (c *transport) handleReply(body epp.Body) error {
+ switch body := body.(type) {
+ case *epp.Response:
+ id := body.TransactionID.Client
+ if id == "" {
+ // TODO: log when server responds with an empty client transaction ID.
+ return TransactionIDError(id)
+ }
+ t, ok := c.popCommand(id)
+ if !ok {
+ // TODO: log when server responds with unknown transaction ID.
+ // TODO: keep abandoned transactions around for some period of time.
+ return TransactionIDError(id)
+ }
+ err := c.finalize(t, body, nil)
+ if err != nil {
+ return err
+ }
+
+ case *epp.Greeting:
+ // Always store the last received from the server.
+ c.greeting.Store(body)
+
+ // Close hasGreeting this is the first recieved.
+ select {
+ case <-c.hasGreeting:
+ default:
+ close(c.hasGreeting)
+ }
+
+ // Pass the to a caller waiting on it.
+ t, ok := c.popHello()
+ if ok {
+ err := c.finalize(t, body, nil)
+ if err != nil {
+ return err
+ }
+ }
+
+ case *epp.Hello:
+ // TODO: log if server receives a or .
+
+ case *epp.Command:
+ // TODO: log if server receives a or .
+ }
+
+ return nil
+}
+
+func (c *transport) finalize(t transaction, body epp.Body, err error) error {
+ select {
+ case <-c.done:
+ return ErrClosedConnection
+ case <-t.ctx.Done():
+ return t.ctx.Err()
+ case t.reply <- reply{body: body, err: err}:
+ }
+ return nil
+}
+
+// pushHello adds a transaction to the end of the stack.
+func (c *transport) pushHello(tx transaction) {
+ c.mHellos.Lock()
+ defer c.mHellos.Unlock()
+ c.hellos = append(c.hellos, tx)
+}
+
+// popHello pops the oldest transaction off the front of the stack.
+func (c *transport) popHello() (transaction, bool) {
+ c.mHellos.Lock()
+ defer c.mHellos.Unlock()
+ if len(c.hellos) == 0 {
+ return transaction{}, false
+ }
+ tx := c.hellos[0]
+ c.hellos = c.hellos[1:]
+ return tx, true
+}
+
+// pushCommand adds a transaction to the map of in-flight commands.
+func (c *transport) pushCommand(id string, tx transaction) error {
+ c.mCommands.Lock()
+ defer c.mCommands.Unlock()
+ _, ok := c.commands[id]
+ if ok {
+ return fmt.Errorf("epp: transaction already exists: %s", id)
+ }
+ c.commands[id] = tx
+ return nil
+}
+
+// popCommand removes a transaction from the map of in-flight commands.
+func (c *transport) popCommand(id string) (transaction, bool) {
+ c.mCommands.Lock()
+ defer c.mCommands.Unlock()
+ tx, ok := c.commands[id]
+ if ok {
+ delete(c.commands, id)
+ }
+ return tx, ok
+}
+
+// cleanup cleans up and responds to all in-flight and transactions.
+// Each transaction will be finalized with err, which may be nil.
+func (c *transport) cleanup(err error) {
+ c.mHellos.Lock()
+ hellos := c.hellos
+ c.hellos = nil
+ c.mHellos.Unlock()
+ for _, tx := range hellos {
+ c.finalize(tx, nil, err)
+ }
+
+ c.mCommands.Lock()
+ commands := c.commands
+ c.commands = nil
+ c.mCommands.Unlock()
+ for _, tx := range commands {
+ c.finalize(tx, nil, err)
+ }
+}
diff --git a/xml.go b/xml.go
deleted file mode 100644
index 73f0343..0000000
--- a/xml.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package epp
-
-import "encoding/xml"
-
-const (
- // EPP defines the IETF URN for the EPP namespace.
- // https://www.iana.org/assignments/xml-registry/ns/epp-1.0.txt
- EPP = `urn:ietf:params:xml:ns:epp-1.0`
-
- // EPPCommon defines the IETF URN for the EPP Common namespace.
- // https://www.iana.org/assignments/xml-registry/ns/eppcom-1.0.txt
- EPPCommon = `urn:ietf:params:xml:ns:eppcom-1.0`
-
- startEPP = ``
- endEPP = ``
- xmlCommandPrefix = xml.Header + startEPP + ``
- xmlCommandSuffix = `` + endEPP
-)
diff --git a/xml_test.go b/xml_test.go
deleted file mode 100644
index 66b83e4..0000000
--- a/xml_test.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package epp
-
-import (
- "encoding/xml"
- "testing"
-
- "github.com/nbio/st"
-)
-
-func TestMarshalOmitEmpty(t *testing.T) {
- v := struct {
- XMLName struct{} `xml:"hello"`
- Foo string `xml:"foo"`
- Bar struct {
- Baz string `xml:"baz"`
- } `xml:"bar,omitempty"`
- }{}
-
- x, err := xml.Marshal(&v)
- st.Expect(t, err, nil)
- st.Expect(t, string(x), ``)
-}