blob: 5eab431d7e98e469a8d1042870af719958d96ff2 [file] [log] [blame]
package ident
import (
"fmt"
"regexp"
"strconv"
"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,
// ie. the port local to the ident client.
ClientPort uint16
// ServerPort is the port number on the server side of the ident protocol,
// ie. the port local to the ident server.
ServerPort uint16
// Exactly one of {Error, Ident} must be non-zero.
// Error is either NoError (the zero value) or one of the ErrorResponse
// types if this response represents an ident protocol error reply.
Error ErrorResponse
// Ident is either nil or a IdentResponse if this response represents an
// ident protocol ident reply.
Ident *IdentResponse
}
// ErrorResponse is error-type from RFC1413, indicating one of the possible
// errors returned by the ident protocol server.
type ErrorResponse string
const (
// NoError is an ErrorResponse that indicates a lack of error.
NoError ErrorResponse = ""
// InvalidPort indicates that either the local or foreign port was
// improperly specified.
InvalidPort ErrorResponse = "INVALID-PORT"
// NoUser indicates that the port pair is not currently in use or currently
// not owned by an identifiable entity.
NoUser ErrorResponse = "NO-USER"
// HiddenUser indicates that the server was able to identify the user of
// this port, but the information was not returned at the request of the
// user.
HiddenUser ErrorResponse = "HIDDEN-USER"
// UnknownError indicates that the server could not determine the
// connection owner for an unknown reason.
UnknownError ErrorResponse = "UNKNOWN-ERROR"
)
// IsStandardError returns whether ErrorResponse represents a standard error.
func (e ErrorResponse) IsStandardError() bool {
switch e {
case InvalidPort, NoUser, HiddenUser, UnknownError:
return true
default:
return false
}
}
// IsNonStandardError returns ehther the ErrorResponse represents a
// non-standard error.
func (e ErrorResponse) IsNonStandardError() bool {
return len(e) > 0 && e[0] == 'X'
}
func (e ErrorResponse) IsError() bool {
if e.IsStandardError() {
return true
}
if e.IsNonStandardError() {
return true
}
return false
}
// IdentResponse is the combined opsys, charset and user-id fields from
// RFC1413. It represents a non-error response from the ident protocol server.
type IdentResponse struct {
// OperatingSystem is an operating system identifier as per RFC1340. This
// is usually UNIX. OTHER has a special meaning, see RFC1413 for more
// information.
OperatingSystem string
// CharacterSet a character set as per RFC1340, defaulting to US-ASCII.
CharacterSet string
// UserID is the 'normal' user identification of the owner of the
// connection, unless the operating system is set to OTHER. See RFC1413 for
// more information.
UserID string
}
// 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) {
if match := reErrorReply.FindStringSubmatch(string(data)); match != nil {
serverPort, err := strconv.ParseUint(match[1], 10, 16)
if err != nil {
return nil, fmt.Errorf("invalid server port: %w", err)
}
clientPort, err := strconv.ParseUint(match[2], 10, 16)
if err != nil {
return nil, fmt.Errorf("invalid client port: %w", err)
}
errResp := ErrorResponse(strings.TrimSpace(match[3]))
if !errResp.IsError() {
// The RFC doesn't tell us what we should do in this case. For
// reliability, we downcast any unknown error to UNKNOWN-ERROR.
errResp = UnknownError
}
return &Response{
ClientPort: uint16(clientPort),
ServerPort: uint16(serverPort),
Error: errResp,
}, nil
}
if match := reIdentReply.FindStringSubmatch(string(data)); match != nil {
serverPort, err := strconv.ParseUint(match[1], 10, 16)
if err != nil {
return nil, fmt.Errorf("invalid server port: %w", err)
}
clientPort, err := strconv.ParseUint(match[2], 10, 16)
if err != nil {
return nil, fmt.Errorf("invalid client port: %w", err)
}
os := strings.TrimSpace(match[3])
charset := strings.TrimSpace(match[5])
if charset == "" {
charset = "US-ASCII"
}
userid := strings.TrimSpace(match[6])
return &Response{
ClientPort: uint16(clientPort),
ServerPort: uint16(serverPort),
Ident: &IdentResponse{
OperatingSystem: os,
CharacterSet: charset,
UserID: userid,
},
}, nil
}
return nil, fmt.Errorf("unparseable response")
}