cluster/identd/ident: add basic ident protocol server

This adds an ident protocol server and tests for it.

Change-Id: I830f85faa7dce4220bd7001635b20e88b4a8b417
diff --git a/cluster/identd/ident/response.go b/cluster/identd/ident/response.go
index 5eab431..f54b728 100644
--- a/cluster/identd/ident/response.go
+++ b/cluster/identd/ident/response.go
@@ -7,17 +7,6 @@
 	"strings"
 )
 
-var (
-	// reErrorReply matches error-reply from RFC1413, but also allows extra
-	// whitespace between significant tokens. It does not ensure that the
-	// error-type is one of the standardized values.
-	reErrorReply = regexp.MustCompile(`^\s*(\d{1,5})\s*,\s*(\d{1,5})\s*:\s*ERROR\s*:\s*(.+)$`)
-	// reIdentReply matches ident-reply from RFC1413, but also allows extra
-	// whitespace between significant tokens. It does not ensure that that
-	// opsys-field and user-id parts are RFC compliant.
-	reIdentReply = regexp.MustCompile(`^\s*(\d{1,5})\s*,\s*(\d{1,5})\s*:\s*USERID\s*:\s*([^:,]+)(,([^:]+))?\s*:(.+)$`)
-)
-
 // Response is an ident protocol response, as seen by the client or server.
 type Response struct {
 	// ClientPort is the port number on the client side of the indent protocol,
@@ -100,6 +89,47 @@
 	UserID string
 }
 
+// encode encodes the given Response. If the Response is unencodable/malformed,
+// nil is returned.
+func (r *Response) encode() []byte {
+	// Both Error and Ident cannot be set at once.
+	if r.Error != "" && r.Ident != nil {
+		return nil
+	}
+
+	if r.Error != "" {
+		if !r.Error.IsError() {
+			return nil
+		}
+		return []byte(fmt.Sprintf("%d,%d:ERROR:%s\r\n", r.ServerPort, r.ClientPort, r.Error))
+	}
+	if r.Ident != nil {
+		id := r.Ident
+		os := id.OperatingSystem
+		if os == "" {
+			return nil
+		}
+		// For compatibility, do not set US-ASCII explicitly.
+		if id.CharacterSet != "" && id.CharacterSet != "US-ASCII" {
+			os += "," + id.CharacterSet
+		}
+		return []byte(fmt.Sprintf("%d,%d:USERID:%s:%s\r\n", r.ServerPort, r.ClientPort, os, id.UserID))
+	}
+	// Malformed response, return nil.
+	return nil
+}
+
+var (
+	// reErrorReply matches error-reply from RFC1413, but also allows extra
+	// whitespace between significant tokens. It does not ensure that the
+	// error-type is one of the standardized values.
+	reErrorReply = regexp.MustCompile(`^\s*(\d{1,5})\s*,\s*(\d{1,5})\s*:\s*ERROR\s*:\s*(.+)$`)
+	// reIdentReply matches ident-reply from RFC1413, but also allows extra
+	// whitespace between significant tokens. It does not ensure that that
+	// opsys-field and user-id parts are RFC compliant.
+	reIdentReply = regexp.MustCompile(`^\s*(\d{1,5})\s*,\s*(\d{1,5})\s*:\s*USERID\s*:\s*([^:,]+)(,([^:]+))?\s*:(.+)$`)
+)
+
 // decodeResponse parses the given bytes as an ident response. The data must be
 // stripped of the trailing \r\n.
 func decodeResponse(data []byte) (*Response, error) {