v2 / vlib / net / ftp / ftp.v
287 lines · 265 sloc · 5.81 KB · 8e35f4d9848f7ad35d857a187dddbfd2eca5e19d
Raw
1module ftp
2
3/*
4basic ftp module
5 RFC-959
6 https://tools.ietf.org/html/rfc959
7
8 Methods:
9 ftp.connect(host)
10 ftp.login(user, passw)
11 pwd := ftp.pwd()
12 ftp.cd(folder)
13 dtp := ftp.pasv()
14 ftp.dir()
15 ftp.get(file)
16 dtp.read()
17 dtp.close()
18 ftp.close()
19*/
20import net
21import io
22
23const connected = 220
24const specify_password = 331
25const logged_in = 230
26const login_first = 503
27const anonymous = 530
28const open_data_connection = 150
29const close_data_connection = 226
30const command_ok = 200
31const denied = 550
32const passive_mode = 227
33const complete = 226
34
35struct DTP {
36mut:
37 conn &net.TcpConn = unsafe { nil }
38 reader io.BufferedReader
39 ip string
40 port int
41}
42
43fn (mut dtp DTP) read() ![]u8 {
44 mut data := []u8{}
45 mut buf := []u8{len: 1024}
46 for {
47 len := dtp.reader.read(mut buf) or { break }
48 if len == 0 {
49 break
50 }
51 data << buf[..len]
52 }
53 return data
54}
55
56fn (mut dtp DTP) close() {
57 if dtp.conn != unsafe { nil } {
58 dtp.conn.close() or { panic(err) }
59 }
60}
61
62struct FTP {
63mut:
64 conn &net.TcpConn = unsafe { nil }
65 reader io.BufferedReader
66 buffer_size int
67}
68
69// new returns an `FTP` instance.
70pub fn new() FTP {
71 mut f := FTP{
72 conn: unsafe { nil }
73 reader: io.new_buffered_reader(reader: unsafe { nil })
74 }
75 f.buffer_size = 1024
76 return f
77}
78
79fn (mut zftp FTP) write(data string) !int {
80 $if debug {
81 println('FTP.v >>> ${data}')
82 }
83 return zftp.conn.write('${data}\r\n'.bytes())
84}
85
86fn (mut zftp FTP) read() !(int, string) {
87 mut data := zftp.reader.read_line()!
88 $if debug {
89 println('FTP.v <<< ${data}')
90 }
91 if data.len < 5 {
92 return 0, ''
93 }
94 code := data[..3].int()
95 if data[3] == `-` {
96 for {
97 data = zftp.reader.read_line()!
98 if data[..3].int() == code && data[3] != `-` {
99 break
100 }
101 }
102 }
103 return code, data
104}
105
106// connect establishes an FTP connection to the host at `oaddress` (ip:port).
107pub fn (mut zftp FTP) connect(oaddress string) !bool {
108 zftp.conn = net.dial_tcp(oaddress)!
109 zftp.reader = io.new_buffered_reader(reader: zftp.conn)
110 code, _ := zftp.read()!
111 if code == connected {
112 return true
113 }
114 return false
115}
116
117// login sends the "USER `user`" and "PASS `passwd`" commands to the remote host.
118pub fn (mut zftp FTP) login(user string, passwd string) !bool {
119 zftp.write('USER ${user}') or {
120 $if debug {
121 println('ERROR sending user')
122 }
123 return false
124 }
125 mut code, _ := zftp.read()!
126 if code == logged_in {
127 return true
128 }
129 if code != specify_password {
130 return false
131 }
132 zftp.write('PASS ${passwd}') or {
133 $if debug {
134 println('ERROR sending password')
135 }
136 return false
137 }
138 code, _ = zftp.read()!
139 if code == logged_in {
140 return true
141 }
142 return false
143}
144
145// close closes the FTP connection.
146pub fn (mut zftp FTP) close() ! {
147 if zftp.conn != unsafe { nil } {
148 zftp.write('QUIT')!
149 zftp.conn.close()!
150 }
151}
152
153// pwd returns the current working directory on the remote host for the logged in user.
154pub fn (mut zftp FTP) pwd() !string {
155 zftp.write('PWD')!
156 _, data := zftp.read()!
157 spl := data.split('"') // "
158 if spl.len >= 2 {
159 return spl[1]
160 }
161 return data
162}
163
164// cd changes the current working directory to the specified remote directory `dir`.
165pub fn (mut zftp FTP) cd(dir string) ! {
166 zftp.write('CWD ${dir}') or { return }
167 mut code, mut data := zftp.read()!
168 match int(code) {
169 denied {
170 $if debug {
171 println('CD ${dir} denied!')
172 }
173 }
174 complete {
175 code, data = zftp.read()!
176 }
177 else {}
178 }
179
180 $if debug {
181 println('CD ${data}')
182 }
183}
184
185fn new_dtp(msg string) !&DTP {
186 if !is_dtp_message_valid(msg) {
187 return error('Bad message')
188 }
189 ip, port := get_host_ip_from_dtp_message(msg)
190 mut dtp := &DTP{
191 ip: ip
192 port: port
193 conn: unsafe { nil }
194 reader: io.new_buffered_reader(reader: unsafe { nil })
195 }
196 conn := net.dial_tcp('${ip}:${port}') or { return error('Cannot connect to the data channel') }
197 dtp.conn = conn
198 dtp.reader = io.new_buffered_reader(reader: dtp.conn)
199 return dtp
200}
201
202fn (mut zftp FTP) pasv() !&DTP {
203 zftp.write('PASV')!
204 code, data := zftp.read()!
205 $if debug {
206 println('pass: ${data}')
207 }
208 if code != passive_mode {
209 return error('passive mode not allowed')
210 }
211 dtp := new_dtp(data)!
212 return dtp
213}
214
215// dir returns a list of the files in the current working directory.
216pub fn (mut zftp FTP) dir() ![]string {
217 mut dtp := zftp.pasv() or { return error('Cannot establish data connection') }
218 zftp.write('LIST')!
219 code, _ := zftp.read()!
220 if code == denied {
221 return error('`LIST` denied')
222 }
223 if code != open_data_connection {
224 return error('Data channel empty')
225 }
226 list_dir := dtp.read()!
227 result, _ := zftp.read()!
228 if result != close_data_connection {
229 println('`LIST` not ok')
230 }
231 dtp.close()
232 mut dir := []string{}
233 sdir := list_dir.bytestr()
234 for lfile in sdir.split('\n') {
235 if lfile.len > 56 {
236 dir << lfile#[56..lfile.len - 1]
237 continue
238 }
239 if lfile.len > 1 {
240 trimmed := lfile.after(':')
241 dir << trimmed#[3..trimmed.len - 1]
242 }
243 }
244 return dir
245}
246
247// get retrieves `file` from the remote host.
248pub fn (mut zftp FTP) get(file string) ![]u8 {
249 mut dtp := zftp.pasv() or { return error('Cannot stablish data connection') }
250 zftp.write('RETR ${file}')!
251 code, _ := zftp.read()!
252 if code == denied {
253 return error('Permission denied')
254 }
255 if code != open_data_connection {
256 return error('Data connection not ready')
257 }
258 blob := dtp.read()!
259 result, _ := zftp.read()!
260 if result != complete {
261 return error('`RETR` not ok')
262 }
263 dtp.close()
264 return blob
265}
266
267fn is_dtp_message_valid(msg string) bool {
268 // An example of message:
269 // '227 Entering Passive Mode (209,132,183,61,48,218)'
270 return msg.contains('(') && msg.contains(')') && msg.contains(',')
271}
272
273fn get_host_ip_from_dtp_message(msg string) (string, int) {
274 mut par_start_idx := -1
275 mut par_end_idx := -1
276 for i, c in msg {
277 if c == `(` {
278 par_start_idx = i + 1
279 } else if c == `)` {
280 par_end_idx = i
281 }
282 }
283 data := msg[par_start_idx..par_end_idx].split(',')
284 ip := data[0..4].join('.')
285 port := data[4].int() * 256 + data[5].int()
286 return ip, port
287}
288