-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathtelnet.go
284 lines (220 loc) · 7.37 KB
/
telnet.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
package telnet
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"net"
"strings"
"time"
)
// MaxCommandLen is an artificial restriction, but it will help in case of
// random large queries.
const MaxCommandLen = 1000
// DefaultDialTimeout provides default auth timeout to remote server.
const DefaultDialTimeout = 5 * time.Second
// DefaultExitCommand provides default TELNET exit command.
const DefaultExitCommand = "exit"
// ForcedExitCommand provides forced TELNET exit command.
const ForcedExitCommand = ":q"
// CRLF moves the cursor to the next line and then moves it to the beginning.
const CRLF = "\r\n"
// NullString is a null byte in string format.
const NullString = "\x00"
// ReceiveWaitPeriod is a delay to receive data from the server.
const ReceiveWaitPeriod = 3 * time.Millisecond
// ExecuteTickTimeout is execute read timeout.
const ExecuteTickTimeout = 1 * time.Second
// Remote server response messages.
const (
ResponseEnterPassword = "Please enter password"
ResponseAuthSuccess = "Logon successful."
ResponseAuthIncorrectPassword = "Password incorrect, please enter password:"
ResponseAuthTooManyFails = "Too many failed login attempts!"
ResponseWelcome = "Press 'help' to get a list of all commands. Press 'exit' to end session."
// ResponseINFLayout is the template for the logline about the command
// received by the server.
ResponseINFLayout = "INF Executing command '%s' by Telnet from %s"
)
var (
// ErrAuthFailed is returned when 7 Days to Die server rejected
// sent password.
ErrAuthFailed = errors.New("authentication failed")
// ErrAuthUnexpectedMessage is returned when 7 Days to Die server responses
// without ResponseAuthSuccess or ResponseAuthIncorrectPassword
// on auth request.
ErrAuthUnexpectedMessage = errors.New("unexpected authentication response")
// ErrCommandTooLong is returned when executed command length is bigger
// than MaxCommandLen characters.
ErrCommandTooLong = errors.New("command too long")
// ErrCommandEmpty is returned when executed command length equal 0.
ErrCommandEmpty = errors.New("command too small")
// ErrMultiErrorOccurred is returned when close connection failed with
// error after auth failed.
ErrMultiErrorOccurred = errors.New("an error occurred while handling another error")
)
// Conn is TELNET connection.
type Conn struct {
conn net.Conn
settings Settings
reader io.Reader
writer io.Writer
buffer *bytes.Buffer
status string
}
// Dial creates a new authorized TELNET connection.
func Dial(address string, password string, options ...Option) (*Conn, error) {
settings := DefaultSettings
for _, option := range options {
option(&settings)
}
conn, err := net.DialTimeout("tcp", address, settings.dialTimeout)
if err != nil {
// Failed to open TCP conn to the server.
return nil, fmt.Errorf("telnet: %w", err)
}
client := Conn{conn: conn, settings: settings, reader: conn, writer: conn, buffer: new(bytes.Buffer)}
go client.processReadResponse(client.buffer)
if err := client.auth(password); err != nil {
// Failed to auth conn with the server.
if err2 := client.Close(); err2 != nil {
//nolint:errorlint // TODO: Come up with the better wrapping
return &client, fmt.Errorf("%w: %v. Previous error: %v", ErrMultiErrorOccurred, err2, err)
}
return &client, err
}
return &client, nil
}
// DialInteractive parses commands from input reader, executes them on remote
// server and writes responses to output writer. Password can be empty string.
// In this case password will be prompted in an interactive window.
func DialInteractive(r io.Reader, w io.Writer, address string, password string, options ...Option) error {
settings := DefaultSettings
for _, option := range options {
option(&settings)
}
conn, err := net.DialTimeout("tcp", address, settings.dialTimeout)
if err != nil {
// Failed to open TCP conn to the server.
return fmt.Errorf("telnet: %w", err)
}
client := Conn{conn: conn, settings: settings, reader: conn, writer: conn}
defer client.Close()
if password != "" {
if _, err := client.write([]byte(password + CRLF)); err != nil {
return err
}
}
go client.processReadResponse(w)
return client.interactive(r)
}
// Execute sends command string to execute to the remote TELNET server.
func (c *Conn) Execute(command string) (string, error) {
if command == "" {
return "", ErrCommandEmpty
}
response, err := c.execute(command)
if err != nil {
return response, err
}
if c.settings.clearResponse {
responseINFMessage := fmt.Sprintf(ResponseINFLayout+CRLF, command, c.LocalAddr().String())
if tmp := strings.Split(response, responseINFMessage); len(tmp) > 1 {
return tmp[1], err
}
}
return response, err
}
// LocalAddr returns the local network address.
func (c *Conn) LocalAddr() net.Addr {
return c.conn.LocalAddr()
}
// RemoteAddr returns the remote network address.
func (c *Conn) RemoteAddr() net.Addr {
return c.conn.RemoteAddr()
}
// Status returns server info status after auth request.
func (c *Conn) Status() string {
return c.status
}
// Close closes the client connection.
func (c *Conn) Close() error {
_, _ = c.write([]byte(c.settings.exitCommand + CRLF))
time.Sleep(ReceiveWaitPeriod)
return c.conn.Close()
}
// auth authenticates client for the next requests.
func (c *Conn) auth(password string) error {
var err error
if c.status, err = c.execute(password); err != nil {
return err
}
if strings.Contains(c.status, ResponseAuthIncorrectPassword) {
return ErrAuthFailed
}
if !strings.Contains(c.status, ResponseAuthSuccess) {
return ErrAuthUnexpectedMessage
}
c.status = strings.TrimPrefix(c.status, ResponseEnterPassword+CRLF+ResponseAuthSuccess)
c.status = strings.TrimSuffix(c.status, CRLF+CRLF+ResponseWelcome)
c.status = strings.TrimSpace(c.status)
return nil
}
// execute sends command string to execute to the remote TELNET server.
func (c *Conn) execute(command string) (string, error) {
if len(command) > MaxCommandLen {
return "", ErrCommandTooLong
}
if _, err := c.write([]byte(command + CRLF)); err != nil {
return "", err
}
time.Sleep(ExecuteTickTimeout)
response := c.buffer.String()
*c.buffer = bytes.Buffer{}
response = strings.ReplaceAll(response, NullString, "")
response = strings.TrimSpace(response)
return response, nil
}
// interactive reads commands from reader in terminal mode and sends them
// to execute to the remote TELNET server.
func (c *Conn) interactive(r io.Reader) error {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
command := scanner.Text()
if command == ForcedExitCommand {
command = c.settings.exitCommand
}
if _, err := c.write([]byte(command + CRLF)); err != nil {
return err
}
if command == c.settings.exitCommand {
break
}
}
time.Sleep(ReceiveWaitPeriod)
return c.Close()
}
// write sends data to established TELNET connection.
func (c *Conn) write(p []byte) (n int, err error) {
return c.writer.Write(p)
}
// read reads structured binary data from c.conn into byte array.
func (c *Conn) read(p []byte) (n int, err error) {
return c.reader.Read(p)
}
// processReadResponse reads response data from TELNET connection
// and writes them to writer (Stdout).
func (c *Conn) processReadResponse(writer io.Writer) {
packet := make([]byte, 1)
for {
// Read 1 byte.
n, err := c.read(packet)
if n <= 0 && err == nil {
continue
} else if n <= 0 && err != nil {
break
}
_, _ = writer.Write(packet)
}
}