diff --git a/check.go b/check.go index a5a7249..26c9bee 100644 --- a/check.go +++ b/check.go @@ -23,13 +23,12 @@ func (c *Conn) CheckDomainExtensions(domains []string, extData map[string]string return nil, err } - err = c.writeDataUnit(x) + err = c.writeRequest(x) if err != nil { return nil, err } - var res Response - err = c.readResponse(&res) + res, err := c.readResponse() if err != nil { return nil, err } @@ -41,12 +40,11 @@ func (c *Conn) CheckDomainExtensions(domains []string, extData map[string]string if err != nil { return nil, err } - err = c.writeDataUnit(x) + err = c.writeRequest(x) if err != nil { return nil, err } - var res2 Response - err = c.readResponse(&res2) + res2, err := c.readResponse() if err != nil { return nil, err } diff --git a/conn.go b/conn.go index 4dd722c..520e62e 100644 --- a/conn.go +++ b/conn.go @@ -1,11 +1,11 @@ package epp import ( - "bytes" "encoding/binary" "encoding/xml" "io" "net" + "sync" "time" ) @@ -19,153 +19,161 @@ func IgnoreEOF(err error) error { } // Conn represents a single connection to an EPP server. -// This implementation is not safe for concurrent use. +// 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 - buf bytes.Buffer - decoder *xml.Decoder - saved xml.Decoder + + // 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 + + // m protects Greeting and LoginResult. + m sync.Mutex // 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 // 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 - // Timeout defines the timeout for network operations. - Timeout time.Duration + // mWrite synchronizes connection writes. + mWrite sync.Mutex + + responses chan *Response + readErr error + + done chan struct{} } // 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) { - c := newConn(conn) - g, err := c.readGreeting() - if err == nil { - c.Greeting = g - } - return c, err + return NewTimeoutConn(conn, 0) } // 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 := newConn(conn) - c.Timeout = timeout + c := &Conn{ + Conn: conn, + Timeout: timeout, + responses: make(chan *Response), + done: make(chan struct{}), + } + go c.readLoop() g, err := c.readGreeting() if err == nil { + c.m.Lock() c.Greeting = g + c.m.Unlock() } return c, err } // Close sends an EPP command and closes the connection c. func (c *Conn) Close() error { + select { + case <-c.done: + return net.ErrClosed + default: + } c.Logout() + close(c.done) return c.Conn.Close() } -// newConn initializes an epp.Conn from a net.Conn. -// Used internally for testing. -func newConn(conn net.Conn) *Conn { - c := Conn{Conn: conn} - c.decoder = xml.NewDecoder(&c.buf) - c.saved = *c.decoder - return &c +// 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)) + } + return writeDataUnit(c.Conn, x) } -// reset resets the underlying xml.Decoder and bytes.Buffer, -// restoring the original state of the underlying -// xml.Decoder (pos 1, line 1, stack, etc.) using a hack. -func (c *Conn) reset() { - c.buf.Reset() - *c.decoder = c.saved // Heh. +// 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) { + select { + case res := <-c.responses: + if res == nil { + return res, c.readErr + } + if res.Result.IsError() { + return nil, &res.Result + } + return res, nil + case <-c.done: + return nil, net.ErrClosed + } } -// writeDataUnit writes a slice of bytes to c. +func (c *Conn) readLoop() { + defer close(c.responses) + timeout := c.Timeout + r := &io.LimitedReader{R: c.Conn} + decoder := xml.NewDecoder(r) + for { + if timeout > 0 { + c.Conn.SetReadDeadline(time.Now().Add(timeout)) + } + n, err := readDataUnitHeader(c.Conn) + if err != nil { + c.readErr = err + return + } + r.N = int64(n) + res := &Response{} + err = IgnoreEOF(scanResponse.Scan(decoder, res)) + if err != nil { + c.readErr = err + return + } + c.responses <- res + } +} + +// writeDataUnit writes x 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 (c *Conn) writeDataUnit(x []byte) error { +func writeDataUnit(w io.Writer, x []byte) error { logXML("<-- WRITE DATA UNIT -->", x) s := uint32(4 + len(x)) - if c.Timeout > 0 { - c.Conn.SetWriteDeadline(time.Now().Add(c.Timeout)) - } - err := binary.Write(c.Conn, binary.BigEndian, s) + err := binary.Write(w, binary.BigEndian, s) if err != nil { return err } - _, err = c.Conn.Write(x) + _, err = w.Write(x) return err } -// readResponse reads a single EPP response from c and parses the XML into req. -// It returns an error if the EPP response contains an error Result. -func (c *Conn) readResponse(res *Response) error { - err := c.readDataUnit() - if err != nil { - return err - } - err = IgnoreEOF(scanResponse.Scan(c.decoder, res)) - if err != nil { - return err - } - if res.Result.IsError() { - return &res.Result - } - return nil -} - -// readDataUnit reads a single EPP message from c into -// c.buf. The bytes in c.buf are valid until the next -// call to readDataUnit. -func (c *Conn) readDataUnit() error { - c.reset() - var s int32 - if c.Timeout > 0 { - c.Conn.SetReadDeadline(time.Now().Add(c.Timeout)) - } - err := binary.Read(c.Conn, binary.BigEndian, &s) - if err != nil { - return err - } - s -= 4 // https://tools.ietf.org/html/rfc5734#section-4 - if s < 0 { - return io.ErrUnexpectedEOF - } - lr := io.LimitedReader{R: c.Conn, N: int64(s)} - n, err := c.buf.ReadFrom(&lr) +// 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 err + return 0, err } - if n != int64(s) || lr.N != 0 { - return io.ErrUnexpectedEOF + if n < 4 { + return 0, io.ErrUnexpectedEOF } - logXML("<-- READ DATA UNIT -->", c.buf.Bytes()) - return nil -} - -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] -} - -func deleteBufferRange(buf *bytes.Buffer, pfx, sfx []byte) { - v := deleteRange(buf.Bytes(), pfx, sfx) - buf.Truncate(len(v)) + // https://tools.ietf.org/html/rfc5734#section-4 + return n - 4, err } diff --git a/conn_test.go b/conn_test.go index 26fbb19..c008277 100644 --- a/conn_test.go +++ b/conn_test.go @@ -1,7 +1,7 @@ package epp import ( - "encoding/xml" + "bytes" "net" "sync" "testing" @@ -34,7 +34,7 @@ func (ls *localServer) teardown() { } func newLocalServer() (*localServer, error) { - ln, err := net.Listen("tcp", ":0") + ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return nil, err } @@ -48,13 +48,11 @@ func TestNewConn(t *testing.T) { ls.buildup(func(ls *localServer, ln net.Listener) { conn, err := ls.Accept() st.Assert(t, err, nil) - sc := newConn(conn) // Respond with greeting - err = sc.writeDataUnit([]byte(testXMLGreeting)) + err = writeDataUnit(conn, []byte(testXMLGreeting)) st.Assert(t, err, nil) - var res Response // Read logout message - err = sc.readResponse(&res) + _, err = readDataUnitHeader(conn) st.Assert(t, err, nil) // Close connection err = conn.Close() @@ -62,7 +60,6 @@ func TestNewConn(t *testing.T) { }) 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) @@ -71,39 +68,6 @@ func TestNewConn(t *testing.T) { st.Expect(t, err, nil) } -func TestConnDecoderReuse(t *testing.T) { - c := newConn(nil) - v := struct { - XMLName struct{} `xml:"hello"` - Foo string `xml:"foo"` - }{} - - c.reset() - c.buf.WriteString(`foo`) - st.Expect(t, c.decoder.InputOffset(), int64(0)) - c.decoder.Decode(&v) - st.Expect(t, v.Foo, "foo") - st.Expect(t, c.decoder.InputOffset(), int64(29)) - - c.reset() - c.buf.WriteString(`bar`) - st.Expect(t, c.decoder.InputOffset(), int64(0)) - tok, _ := c.decoder.Token() - se := tok.(xml.StartElement) - st.Expect(t, se.Name.Local, "hello") - tok, _ = c.decoder.Token() - se = tok.(xml.StartElement) - st.Expect(t, se.Name.Local, "foo") - st.Expect(t, c.decoder.InputOffset(), int64(12)) - - c.reset() - c.buf.WriteString(`blam<`) - st.Expect(t, c.decoder.InputOffset(), int64(0)) - c.decoder.Decode(&v) - st.Expect(t, v.Foo, "blam<") - st.Expect(t, c.decoder.InputOffset(), int64(34)) -} - func TestDeleteRange(t *testing.T) { v := deleteRange([]byte(``), []byte(``)) st.Expect(t, string(v), ``) @@ -111,3 +75,23 @@ func TestDeleteRange(t *testing.T) { 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/greeting.go b/greeting.go index 3ddbcf9..78d93ea 100644 --- a/greeting.go +++ b/greeting.go @@ -8,7 +8,7 @@ import ( // Hello sends a command to request a from the EPP server. func (c *Conn) Hello() error { - err := c.writeDataUnit(xmlHello) + err := c.writeRequest(xmlHello) if err != nil { return err } @@ -102,14 +102,9 @@ var ExtURNNames = map[string]string{ "neulevel-1.0": ExtNeulevel10, } +// TODO: check if res.Greeting is not empty. func (c *Conn) readGreeting() (Greeting, error) { - err := c.readDataUnit() - if err != nil { - return Greeting{}, err - } - deleteBufferRange(&c.buf, []byte(``), []byte(``)) - var res Response - err = IgnoreEOF(scanResponse.Scan(c.decoder, &res)) + res, err := c.readResponse() if err != nil { return Greeting{}, err } diff --git a/greeting_test.go b/greeting_test.go index 79f7b46..dbbcfc4 100644 --- a/greeting_test.go +++ b/greeting_test.go @@ -16,12 +16,11 @@ func TestHello(t *testing.T) { ls.buildup(func(ls *localServer, ln net.Listener) { conn, err := ls.Accept() st.Assert(t, err, nil) - sc := newConn(conn) // Respond with greeting - err = sc.writeDataUnit([]byte(testXMLGreeting)) + err = writeDataUnit(conn, []byte(testXMLGreeting)) st.Assert(t, err, nil) // Respond with greeting for - err = sc.writeDataUnit([]byte(testXMLGreeting)) + err = writeDataUnit(conn, []byte(testXMLGreeting)) st.Assert(t, err, nil) }) nc, err := net.Dial(ls.Listener.Addr().Network(), ls.Listener.Addr().String()) diff --git a/info.go b/info.go index 5f6c173..d732cfd 100644 --- a/info.go +++ b/info.go @@ -15,12 +15,11 @@ func (c *Conn) DomainInfo(domain string, extData map[string]string) (*DomainInfo if err != nil { return nil, err } - err = c.writeDataUnit(x) + err = c.writeRequest(x) if err != nil { return nil, err } - var res Response - err = c.readResponse(&res) + res, err := c.readResponse() if err != nil { return nil, err } diff --git a/session.go b/session.go index 1982d27..7c5f4cd 100644 --- a/session.go +++ b/session.go @@ -12,12 +12,16 @@ func (c *Conn) Login(user, password, newPassword string) error { if err != nil { return err } - var res Response - err = c.readResponse(&res) + 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 } @@ -33,7 +37,7 @@ func (c *Conn) writeLogin(user, password, newPassword string) error { if err != nil { return err } - return c.writeDataUnit(x) + return c.writeRequest(x) } func encodeLogin(user, password, newPassword, version, language string, objects, extensions []string) ([]byte, error) { @@ -75,12 +79,12 @@ func encodeLogin(user, password, newPassword, version, language string, objects, // 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.writeDataUnit(xmlLogout) + err := c.writeRequest(xmlLogout) if err != nil { return err } - var res Response - return c.readResponse(&res) + _, err = c.readResponse() + return err } var xmlLogout = []byte(xmlCommandPrefix + `` + xmlCommandSuffix)