| package ident |
| |
| import ( |
| "fmt" |
| "regexp" |
| "strconv" |
| "strings" |
| ) |
| |
| // 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 |
| } |
| |
| // 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) { |
| 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") |
| } |