-
Notifications
You must be signed in to change notification settings - Fork 3
/
ftps.go
361 lines (311 loc) · 7.9 KB
/
ftps.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
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
// Copyright 2020 Daniel Theophanes.
// Use of this source code is governed by a zlib-style
// license that can be found in the LICENSE file.
// Package ftps implements a simple FTPS client.
package ftps
import (
"bufio"
"context"
"crypto/tls"
"fmt"
"io"
"net"
"net/textproto"
"strconv"
"strings"
"unicode"
)
// Client FTPS.
type Client struct {
plain net.Conn
secure *tls.Conn
tc *textproto.Conn
opt DialOptions
}
// DialOptions for the FTPS client.
type DialOptions struct {
Host string
Port int // If zero, this will default to 990.
Username string
Passowrd string
// If true, will connect un-encrypted, then upgrade to using AUTH TLS command.
ExplicitTLS bool
// If true, will NOT attempt to encrypt.
InsecureUnencrypted bool
TLSConfig *tls.Config
}
func joinHostPort(host string, port int) string {
return net.JoinHostPort(host, strconv.FormatInt(int64(port), 10))
}
// Dial a FTPS server and return a Client.
func Dial(ctx context.Context, opt DialOptions) (*Client, error) {
port := opt.Port
if port <= 0 {
if opt.InsecureUnencrypted {
port = 21
} else {
port = 990
}
}
dialer := &net.Dialer{}
dialTo := joinHostPort(opt.Host, port)
conn, err := dialer.DialContext(ctx, "tcp", dialTo)
if err != nil {
return nil, fmt.Errorf("ftps: network dial failed: %w", err)
}
client := &Client{
plain: conn,
opt: opt,
}
if err = client.setup(); err != nil {
client.plain.Close()
return nil, fmt.Errorf("ftps: connection setup failed: %w", err)
}
return client, nil
}
func (c *Client) setup() error {
if c.opt.ExplicitTLS {
c.tc = textproto.NewConn(c.plain)
if _, err := c.read(220); err != nil {
return fmt.Errorf("setup init read: %w", err)
}
if _, err := c.cmd(234, "AUTH TLS"); err != nil {
return err
}
}
if !c.opt.InsecureUnencrypted {
c.secure = tls.Client(c.plain, c.opt.TLSConfig)
if err := c.secure.Handshake(); err != nil {
return err
}
c.tc = textproto.NewConn(c.secure)
} else {
c.tc = textproto.NewConn(c.plain)
}
if !c.opt.ExplicitTLS {
if _, err := c.read(220); err != nil {
return fmt.Errorf("setup init read: %w", err)
}
}
if _, err := c.cmd(331, "USER %s", c.opt.Username); err != nil {
return err
}
if _, err := c.cmd(230, "PASS %s", c.opt.Passowrd); err != nil {
return err
}
if _, err := c.cmd(200, "TYPE I"); err != nil {
return err
}
if _, err := c.cmd(200, "PBSZ %d", 0); err != nil {
return err
}
if c.opt.InsecureUnencrypted {
return nil
}
if _, err := c.cmd(200, "PROT %s", "P"); err != nil {
return err
}
return nil
}
func (c *Client) read(expectCode int) (string, error) {
gotCode, message, err := c.tc.ReadResponse(expectCode)
if err != nil {
return "", fmt.Errorf("failed to read code, got code %d and message %s: %w", gotCode, message, err)
}
return message, nil
}
func (c *Client) cmd(expectedCode int, cmd string, args ...interface{}) (string, error) {
id, err := c.tc.Cmd(cmd, args...)
if err != nil {
return "", fmt.Errorf("cmd %q failed with ID %d: %w", cmd, id, err)
}
message, err := c.read(expectedCode)
if err != nil {
return "", fmt.Errorf("cmd %q failed read expected code %d with message %q: %w", cmd, expectedCode, message, err)
}
return message, nil
}
func (c *Client) data(ctx context.Context, expectedCode int, cmd string, args ...interface{}) (io.ReadWriteCloser, error) {
message, err := c.cmd(227, "PASV")
if err != nil {
return nil, err
}
// Expected Message: Entering Passive Mode (x,x,x,x,p1,p2)
start := strings.Index(message, "(")
end := strings.LastIndex(message, ")")
if start < 0 || end < 0 || end < start {
return nil, fmt.Errorf("invalid PASV response, got %q", message)
}
portPartList := strings.Split(message[start+1:end], ",")
if len(portPartList) < 6 {
return nil, fmt.Errorf("invalid PASV port response, got %q", portPartList)
}
p1, err := strconv.ParseInt(portPartList[4], 10, 16)
if err != nil {
return nil, err
}
p2, err := strconv.ParseInt(portPartList[5], 10, 16)
if err != nil {
return nil, err
}
port := int(p1)*256 + int(p2)
// Ignore the IP address.
dialer := &net.Dialer{}
dconn, err := dialer.DialContext(ctx, "tcp", joinHostPort(c.opt.Host, port))
if err != nil {
return nil, fmt.Errorf("dial data conn failed: %w", err)
}
_, err = c.cmd(expectedCode, cmd, args...)
if err != nil {
dconn.Close()
return nil, err
}
if c.opt.InsecureUnencrypted {
return dconn, nil
}
secure := tls.Client(dconn, c.opt.TLSConfig)
return secure, nil
}
// Close the FTPS client connection.
func (c *Client) Close() error {
_, qerr := c.cmd(221, "QUIT")
if c.secure != nil {
serr := c.secure.Close()
if serr != nil {
return serr
}
return qerr
}
c.plain.Close()
return qerr
}
// Getwd gets the current working directory.
func (c *Client) Getwd() (dir string, err error) {
return c.cmd(257, "PWD")
}
// Chdir changes the current working directory.
func (c *Client) Chdir(dir string) error {
if _, err := c.cmd(250, "CWD %s", dir); err != nil {
return fmt.Errorf("ftps: Chdir failed: %w", err)
}
return nil
}
// Mkdir makes a new directory.
func (c *Client) Mkdir(name string) error {
if _, err := c.cmd(257, "MKD %s", name); err != nil {
return fmt.Errorf("ftps: Mkdir failed: %w", err)
}
return nil
}
// RemoveFile removes a file.
func (c *Client) RemoveFile(name string) error {
if _, err := c.cmd(250, "DELE %s", name); err != nil {
return fmt.Errorf("ftps: RemoveFile failed: %w", err)
}
return nil
}
// RemoveDir removes a directory.
func (c *Client) RemoveDir(name string) error {
if _, err := c.cmd(250, "RMD %s", name); err != nil {
return fmt.Errorf("ftps: RemoveDir failed: %w", err)
}
return nil
}
// File of a directory list.
type File struct {
Name string
}
// List the contents of the current working directory.
func (c *Client) List(ctx context.Context) ([]File, error) {
data, err := c.data(ctx, 1, "LIST") // 150
if err != nil {
return nil, fmt.Errorf("ftps: failed to List, unable to get data conn: %w", err)
}
defer data.Close()
list := make([]File, 0, 3)
reader := bufio.NewReader(data)
for {
select {
default:
case <-ctx.Done():
return list, fmt.Errorf("ftps: List canceled: %w", ctx.Err())
}
line, err := reader.ReadString('\n')
if err == io.EOF {
break
}
f, err := readLine(line)
if err != nil {
return list, fmt.Errorf("ftps: List line parse: %w", err)
}
list = append(list, f)
}
data.Close()
_, err = c.read(2) // 226
if err != nil {
return list, fmt.Errorf("ftps: List ack failed: %w", err)
}
return list, nil
}
func readLine(line string) (File, error) {
f := File{}
line = strings.TrimSpace(line)
filenameIndex := 0
ct := 0
inSP := true
for index, r := range line {
sp := unicode.IsSpace(r)
if inSP == sp {
continue
}
inSP = sp
if sp {
continue
}
ct++
if ct == 9 {
filenameIndex = index
break
}
}
f.Name = line[filenameIndex:]
return f, nil
}
// Upload the contents of Reader to the file name to the current working directory.
func (c *Client) Upload(ctx context.Context, name string, r io.Reader) error {
data, err := c.data(ctx, 1, "STOR %s", name) // 150
if err != nil {
return fmt.Errorf("upload data: %w", err)
}
defer data.Close()
_, err = io.Copy(data, r)
if err != nil {
return fmt.Errorf("upload copy: %w", err)
}
if err = data.Close(); err != nil {
return fmt.Errorf("upload close: %w", err)
}
_, err = c.read(2) // 226
if err != nil {
return fmt.Errorf("upload read: %w", err)
}
return nil
}
// Download the file name from the current working directory to the Writer.
func (c *Client) Download(ctx context.Context, name string, w io.Writer) error {
data, err := c.data(ctx, 1, "RETR %s", name) // 150
if err != nil {
return fmt.Errorf("download data: %w", err)
}
defer data.Close()
_, err = io.Copy(w, data)
if err != nil {
return fmt.Errorf("download copy: %w", err)
}
data.Close()
_, err = c.read(2) // 226
if err != nil {
return fmt.Errorf("download read: %w", err)
}
return nil
}