module ftp /* basic ftp module RFC-959 https://tools.ietf.org/html/rfc959 Methods: ftp.connect(host) ftp.login(user, passw) pwd := ftp.pwd() ftp.cd(folder) dtp := ftp.pasv() ftp.dir() ftp.get(file) dtp.read() dtp.close() ftp.close() */ import net import io const ( connected = 220 specify_password = 331 logged_in = 230 login_first = 503 anonymous = 530 open_data_connection = 150 close_data_connection = 226 command_ok = 200 denied = 550 passive_mode = 227 complete = 226 ) struct DTP { mut: conn &net.TcpConn = unsafe { nil } reader io.BufferedReader ip string port int } fn (mut dtp DTP) read() ?[]u8 { mut data := []u8{} mut buf := []u8{len: 1024} for { len := dtp.reader.read(mut buf) or { break } if len == 0 { break } data << buf[..len] } return data } fn (mut dtp DTP) close() { dtp.conn.close() or { panic(err) } } struct FTP { mut: conn &net.TcpConn = unsafe { nil } reader io.BufferedReader buffer_size int } // new returns an `FTP` instance. pub fn new() FTP { mut f := FTP{ conn: 0 } f.buffer_size = 1024 return f } fn (mut zftp FTP) write(data string) ?int { $if debug { println('FTP.v >>> $data') } return zftp.conn.write('$data\r\n'.bytes()) } fn (mut zftp FTP) read() ?(int, string) { mut data := zftp.reader.read_line()? $if debug { println('FTP.v <<< $data') } if data.len < 5 { return 0, '' } code := data[..3].int() if data[3] == `-` { for { data = zftp.reader.read_line()? if data[..3].int() == code && data[3] != `-` { break } } } return code, data } // connect establishes an FTP connection to the host at `ip` port 21. pub fn (mut zftp FTP) connect(ip string) ?bool { zftp.conn = net.dial_tcp('$ip:21')? zftp.reader = io.new_buffered_reader(reader: zftp.conn) code, _ := zftp.read()? if code == ftp.connected { return true } return false } // login sends the "USER `user`" and "PASS `passwd`" commands to the remote host. pub fn (mut zftp FTP) login(user string, passwd string) ?bool { zftp.write('USER $user') or { $if debug { println('ERROR sending user') } return false } mut code, _ := zftp.read()? if code == ftp.logged_in { return true } if code != ftp.specify_password { return false } zftp.write('PASS $passwd') or { $if debug { println('ERROR sending password') } return false } code, _ = zftp.read()? if code == ftp.logged_in { return true } return false } // close closes the FTP connection. pub fn (mut zftp FTP) close() ? { zftp.write('QUIT')? zftp.conn.close()? } // pwd returns the current working directory on the remote host for the logged in user. pub fn (mut zftp FTP) pwd() ?string { zftp.write('PWD')? _, data := zftp.read()? spl := data.split('"') // " if spl.len >= 2 { return spl[1] } return data } // cd changes the current working directory to the specified remote directory `dir`. pub fn (mut zftp FTP) cd(dir string) ? { zftp.write('CWD $dir') or { return } mut code, mut data := zftp.read()? match int(code) { ftp.denied { $if debug { println('CD $dir denied!') } } ftp.complete { code, data = zftp.read()? } else {} } $if debug { println('CD $data') } } fn new_dtp(msg string) ?&DTP { if !is_dtp_message_valid(msg) { return error('Bad message') } ip, port := get_host_ip_from_dtp_message(msg) mut dtp := &DTP{ ip: ip port: port conn: 0 } conn := net.dial_tcp('$ip:$port') or { return error('Cannot connect to the data channel') } dtp.conn = conn dtp.reader = io.new_buffered_reader(reader: dtp.conn) return dtp } fn (mut zftp FTP) pasv() ?&DTP { zftp.write('PASV')? code, data := zftp.read()? $if debug { println('pass: $data') } if code != ftp.passive_mode { return error('pasive mode not allowed') } dtp := new_dtp(data)? return dtp } // dir returns a list of the files in the current working directory. pub fn (mut zftp FTP) dir() ?[]string { mut dtp := zftp.pasv() or { return error('Cannot establish data connection') } zftp.write('LIST')? code, _ := zftp.read()? if code == ftp.denied { return error('`LIST` denied') } if code != ftp.open_data_connection { return error('Data channel empty') } list_dir := dtp.read()? result, _ := zftp.read()? if result != ftp.close_data_connection { println('`LIST` not ok') } dtp.close() mut dir := []string{} sdir := list_dir.bytestr() for lfile in sdir.split('\n') { if lfile.len > 1 { dir << lfile.after(' ').trim_space() } } return dir } // get retrieves `file` from the remote host. pub fn (mut zftp FTP) get(file string) ?[]u8 { mut dtp := zftp.pasv() or { return error('Cannot stablish data connection') } zftp.write('RETR $file')? code, _ := zftp.read()? if code == ftp.denied { return error('Permission denied') } if code != ftp.open_data_connection { return error('Data connection not ready') } blob := dtp.read()? dtp.close() return blob } fn is_dtp_message_valid(msg string) bool { // An example of message: // '227 Entering Passive Mode (209,132,183,61,48,218)' return msg.contains('(') && msg.contains(')') && msg.contains(',') } fn get_host_ip_from_dtp_message(msg string) (string, int) { mut par_start_idx := -1 mut par_end_idx := -1 for i, c in msg { if c == `(` { par_start_idx = i + 1 } else if c == `)` { par_end_idx = i } } data := msg[par_start_idx..par_end_idx].split(',') ip := data[0..4].join('.') port := data[4].int() * 256 + data[5].int() return ip, port }