blob: f54b7284659f551d6a62ac8e3dc4599aa1fbefca [file] [log] [blame]
Serge Bazanskid4438d62021-05-23 13:37:30 +02001package ident
2
3import (
4 "fmt"
5 "regexp"
6 "strconv"
7 "strings"
8)
9
Serge Bazanskid4438d62021-05-23 13:37:30 +020010// Response is an ident protocol response, as seen by the client or server.
11type 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.
31type ErrorResponse string
32
33const (
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.
52func (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.
63func (e ErrorResponse) IsNonStandardError() bool {
64 return len(e) > 0 && e[0] == 'X'
65}
66
67func (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.
79type 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 Bazanskice2737f2021-05-23 17:15:29 +020092// encode encodes the given Response. If the Response is unencodable/malformed,
93// nil is returned.
94func (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
122var (
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 Bazanskid4438d62021-05-23 13:37:30 +0200133// decodeResponse parses the given bytes as an ident response. The data must be
134// stripped of the trailing \r\n.
135func 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}