Serge Bazanski | d4438d6 | 2021-05-23 13:37:30 +0200 | [diff] [blame] | 1 | package ident |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "regexp" |
| 6 | "strconv" |
| 7 | "strings" |
| 8 | ) |
| 9 | |
Serge Bazanski | d4438d6 | 2021-05-23 13:37:30 +0200 | [diff] [blame] | 10 | // Response is an ident protocol response, as seen by the client or server. |
| 11 | type Response struct { |
| 12 | // ClientPort is the port number on the client side of the indent protocol, |
| 13 | // ie. the port local to the ident client. |
| 14 | ClientPort uint16 |
| 15 | // ServerPort is the port number on the server side of the ident protocol, |
| 16 | // ie. the port local to the ident server. |
| 17 | ServerPort uint16 |
| 18 | |
| 19 | // Exactly one of {Error, Ident} must be non-zero. |
| 20 | |
| 21 | // Error is either NoError (the zero value) or one of the ErrorResponse |
| 22 | // types if this response represents an ident protocol error reply. |
| 23 | Error ErrorResponse |
| 24 | // Ident is either nil or a IdentResponse if this response represents an |
| 25 | // ident protocol ident reply. |
| 26 | Ident *IdentResponse |
| 27 | } |
| 28 | |
| 29 | // ErrorResponse is error-type from RFC1413, indicating one of the possible |
| 30 | // errors returned by the ident protocol server. |
| 31 | type ErrorResponse string |
| 32 | |
| 33 | const ( |
| 34 | // NoError is an ErrorResponse that indicates a lack of error. |
| 35 | NoError ErrorResponse = "" |
| 36 | // InvalidPort indicates that either the local or foreign port was |
| 37 | // improperly specified. |
| 38 | InvalidPort ErrorResponse = "INVALID-PORT" |
| 39 | // NoUser indicates that the port pair is not currently in use or currently |
| 40 | // not owned by an identifiable entity. |
| 41 | NoUser ErrorResponse = "NO-USER" |
| 42 | // HiddenUser indicates that the server was able to identify the user of |
| 43 | // this port, but the information was not returned at the request of the |
| 44 | // user. |
| 45 | HiddenUser ErrorResponse = "HIDDEN-USER" |
| 46 | // UnknownError indicates that the server could not determine the |
| 47 | // connection owner for an unknown reason. |
| 48 | UnknownError ErrorResponse = "UNKNOWN-ERROR" |
| 49 | ) |
| 50 | |
| 51 | // IsStandardError returns whether ErrorResponse represents a standard error. |
| 52 | func (e ErrorResponse) IsStandardError() bool { |
| 53 | switch e { |
| 54 | case InvalidPort, NoUser, HiddenUser, UnknownError: |
| 55 | return true |
| 56 | default: |
| 57 | return false |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | // IsNonStandardError returns ehther the ErrorResponse represents a |
| 62 | // non-standard error. |
| 63 | func (e ErrorResponse) IsNonStandardError() bool { |
| 64 | return len(e) > 0 && e[0] == 'X' |
| 65 | } |
| 66 | |
| 67 | func (e ErrorResponse) IsError() bool { |
| 68 | if e.IsStandardError() { |
| 69 | return true |
| 70 | } |
| 71 | if e.IsNonStandardError() { |
| 72 | return true |
| 73 | } |
| 74 | return false |
| 75 | } |
| 76 | |
| 77 | // IdentResponse is the combined opsys, charset and user-id fields from |
| 78 | // RFC1413. It represents a non-error response from the ident protocol server. |
| 79 | type IdentResponse struct { |
| 80 | // OperatingSystem is an operating system identifier as per RFC1340. This |
| 81 | // is usually UNIX. OTHER has a special meaning, see RFC1413 for more |
| 82 | // information. |
| 83 | OperatingSystem string |
| 84 | // CharacterSet a character set as per RFC1340, defaulting to US-ASCII. |
| 85 | CharacterSet string |
| 86 | // UserID is the 'normal' user identification of the owner of the |
| 87 | // connection, unless the operating system is set to OTHER. See RFC1413 for |
| 88 | // more information. |
| 89 | UserID string |
| 90 | } |
| 91 | |
Serge Bazanski | ce2737f | 2021-05-23 17:15:29 +0200 | [diff] [blame] | 92 | // encode encodes the given Response. If the Response is unencodable/malformed, |
| 93 | // nil is returned. |
| 94 | func (r *Response) encode() []byte { |
| 95 | // Both Error and Ident cannot be set at once. |
| 96 | if r.Error != "" && r.Ident != nil { |
| 97 | return nil |
| 98 | } |
| 99 | |
| 100 | if r.Error != "" { |
| 101 | if !r.Error.IsError() { |
| 102 | return nil |
| 103 | } |
| 104 | return []byte(fmt.Sprintf("%d,%d:ERROR:%s\r\n", r.ServerPort, r.ClientPort, r.Error)) |
| 105 | } |
| 106 | if r.Ident != nil { |
| 107 | id := r.Ident |
| 108 | os := id.OperatingSystem |
| 109 | if os == "" { |
| 110 | return nil |
| 111 | } |
| 112 | // For compatibility, do not set US-ASCII explicitly. |
| 113 | if id.CharacterSet != "" && id.CharacterSet != "US-ASCII" { |
| 114 | os += "," + id.CharacterSet |
| 115 | } |
| 116 | return []byte(fmt.Sprintf("%d,%d:USERID:%s:%s\r\n", r.ServerPort, r.ClientPort, os, id.UserID)) |
| 117 | } |
| 118 | // Malformed response, return nil. |
| 119 | return nil |
| 120 | } |
| 121 | |
| 122 | var ( |
| 123 | // reErrorReply matches error-reply from RFC1413, but also allows extra |
| 124 | // whitespace between significant tokens. It does not ensure that the |
| 125 | // error-type is one of the standardized values. |
| 126 | reErrorReply = regexp.MustCompile(`^\s*(\d{1,5})\s*,\s*(\d{1,5})\s*:\s*ERROR\s*:\s*(.+)$`) |
| 127 | // reIdentReply matches ident-reply from RFC1413, but also allows extra |
| 128 | // whitespace between significant tokens. It does not ensure that that |
| 129 | // opsys-field and user-id parts are RFC compliant. |
| 130 | reIdentReply = regexp.MustCompile(`^\s*(\d{1,5})\s*,\s*(\d{1,5})\s*:\s*USERID\s*:\s*([^:,]+)(,([^:]+))?\s*:(.+)$`) |
| 131 | ) |
| 132 | |
Serge Bazanski | d4438d6 | 2021-05-23 13:37:30 +0200 | [diff] [blame] | 133 | // decodeResponse parses the given bytes as an ident response. The data must be |
| 134 | // stripped of the trailing \r\n. |
| 135 | func decodeResponse(data []byte) (*Response, error) { |
| 136 | if match := reErrorReply.FindStringSubmatch(string(data)); match != nil { |
| 137 | serverPort, err := strconv.ParseUint(match[1], 10, 16) |
| 138 | if err != nil { |
| 139 | return nil, fmt.Errorf("invalid server port: %w", err) |
| 140 | } |
| 141 | clientPort, err := strconv.ParseUint(match[2], 10, 16) |
| 142 | if err != nil { |
| 143 | return nil, fmt.Errorf("invalid client port: %w", err) |
| 144 | } |
| 145 | errResp := ErrorResponse(strings.TrimSpace(match[3])) |
| 146 | if !errResp.IsError() { |
| 147 | // The RFC doesn't tell us what we should do in this case. For |
| 148 | // reliability, we downcast any unknown error to UNKNOWN-ERROR. |
| 149 | errResp = UnknownError |
| 150 | } |
| 151 | return &Response{ |
| 152 | ClientPort: uint16(clientPort), |
| 153 | ServerPort: uint16(serverPort), |
| 154 | Error: errResp, |
| 155 | }, nil |
| 156 | } |
| 157 | if match := reIdentReply.FindStringSubmatch(string(data)); match != nil { |
| 158 | serverPort, err := strconv.ParseUint(match[1], 10, 16) |
| 159 | if err != nil { |
| 160 | return nil, fmt.Errorf("invalid server port: %w", err) |
| 161 | } |
| 162 | clientPort, err := strconv.ParseUint(match[2], 10, 16) |
| 163 | if err != nil { |
| 164 | return nil, fmt.Errorf("invalid client port: %w", err) |
| 165 | } |
| 166 | os := strings.TrimSpace(match[3]) |
| 167 | charset := strings.TrimSpace(match[5]) |
| 168 | if charset == "" { |
| 169 | charset = "US-ASCII" |
| 170 | } |
| 171 | userid := strings.TrimSpace(match[6]) |
| 172 | return &Response{ |
| 173 | ClientPort: uint16(clientPort), |
| 174 | ServerPort: uint16(serverPort), |
| 175 | Ident: &IdentResponse{ |
| 176 | OperatingSystem: os, |
| 177 | CharacterSet: charset, |
| 178 | UserID: userid, |
| 179 | }, |
| 180 | }, nil |
| 181 | } |
| 182 | return nil, fmt.Errorf("unparseable response") |
| 183 | } |