v/vlib/net/ftp/ftp.v

274 lines
5.4 KiB
V
Raw Normal View History

module ftp
2019-12-27 19:08:44 +01:00
/*
2020-05-16 16:12:23 +02:00
basic ftp module
2019-12-27 19:08:44 +01:00
RFC-959
https://tools.ietf.org/html/rfc959
Methods:
ftp.connect(host)
ftp.login(user, passw)
2019-12-27 19:08:44 +01:00
pwd := ftp.pwd()
ftp.cd(folder)
dtp := ftp.pasv()
ftp.dir()
ftp.get(file)
dtp.read()
dtp.close()
ftp.close()
*/
import net
import io
2019-12-27 19:08:44 +01:00
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
2019-12-27 19:08:44 +01:00
)
struct DTP {
mut:
conn &net.TcpConn
reader io.BufferedReader
ip string
port int
2019-12-27 19:08:44 +01:00
}
2022-04-15 14:35:35 +02:00
fn (mut dtp DTP) read() ?[]u8 {
mut data := []u8{}
mut buf := []u8{len: 1024}
2021-01-07 20:21:47 +01:00
for {
len := dtp.reader.read(mut buf) or { break }
if len == 0 {
break
}
data << buf[..len]
}
2019-12-27 19:08:44 +01:00
return data
}
fn (mut dtp DTP) close() {
dtp.conn.close() or { panic(err) }
2019-12-27 19:08:44 +01:00
}
struct FTP {
mut:
conn &net.TcpConn
reader io.BufferedReader
2019-12-27 19:08:44 +01:00
buffer_size int
}
// new returns an `FTP` instance.
2019-12-27 19:08:44 +01:00
pub fn new() FTP {
2021-02-02 08:22:52 +01:00
mut f := FTP{
conn: 0
}
2019-12-27 19:08:44 +01:00
f.buffer_size = 1024
return f
}
fn (mut zftp FTP) write(data string) ?int {
2019-12-27 19:08:44 +01:00
$if debug {
println('FTP.v >>> $data')
}
return zftp.conn.write('$data\r\n'.bytes())
2019-12-27 19:08:44 +01:00
}
fn (mut zftp FTP) read() ?(int, string) {
mut data := zftp.reader.read_line()?
2019-12-27 19:08:44 +01:00
$if debug {
println('FTP.v <<< $data')
}
if data.len < 5 {
return 0, ''
2019-12-27 19:08:44 +01:00
}
2020-01-19 13:53:13 +01:00
code := data[..3].int()
if data[3] == `-` {
2019-12-27 19:08:44 +01:00
for {
data = zftp.reader.read_line()?
2020-01-19 13:53:13 +01:00
if data[..3].int() == code && data[3] != `-` {
2019-12-27 19:08:44 +01:00
break
}
}
}
return code, data
2019-12-27 19:08:44 +01:00
}
// 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 {
2019-12-27 19:08:44 +01:00
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')
}
2019-12-27 19:08:44 +01:00
return false
}
mut code, _ := zftp.read()?
if code == ftp.logged_in {
2019-12-27 19:08:44 +01:00
return true
}
if code != ftp.specify_password {
2019-12-27 19:08:44 +01:00
return false
}
zftp.write('PASS $passwd') or {
$if debug {
println('ERROR sending password')
}
2019-12-27 19:08:44 +01:00
return false
}
code, _ = zftp.read()?
if code == ftp.logged_in {
2019-12-27 19:08:44 +01:00
return true
}
return false
}
// close closes the FTP connection.
pub fn (mut zftp FTP) close() ? {
zftp.write('QUIT')?
zftp.conn.close()?
2019-12-27 19:08:44 +01:00
}
// 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('"') // "
2019-12-27 19:08:44 +01:00
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!')
}
2019-12-27 19:08:44 +01:00
}
ftp.complete {
code, data = zftp.read()?
2019-12-27 19:08:44 +01:00
}
else {}
}
$if debug {
println('CD $data')
}
2019-12-27 19:08:44 +01:00
}
2021-01-07 20:21:47 +01:00
fn new_dtp(msg string) ?&DTP {
if !is_dtp_message_valid(msg) {
return error('Bad message')
2019-12-27 19:08:44 +01:00
}
ip, port := get_host_ip_from_dtp_message(msg)
2021-01-07 20:21:47 +01:00
mut dtp := &DTP{
2019-12-27 19:08:44 +01:00
ip: ip
port: port
2021-02-02 08:22:52 +01:00
conn: 0
2019-12-27 19:08:44 +01:00
}
2021-01-07 20:21:47 +01:00
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)
2019-12-27 19:08:44 +01:00
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 {
2019-12-27 19:08:44 +01:00
return error('pasive mode not allowed')
}
dtp := new_dtp(data)?
2019-12-27 19:08:44 +01:00
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')
2019-12-27 19:08:44 +01:00
}
if code != ftp.open_data_connection {
return error('Data channel empty')
2019-12-27 19:08:44 +01:00
}
list_dir := dtp.read()?
result, _ := zftp.read()?
if result != ftp.close_data_connection {
println('`LIST` not ok')
2019-12-27 19:08:44 +01:00
}
dtp.close()
2020-04-26 13:49:31 +02:00
mut dir := []string{}
sdir := list_dir.bytestr()
2019-12-27 19:08:44 +01:00
for lfile in sdir.split('\n') {
2020-05-16 16:12:23 +02:00
if lfile.len > 1 {
dir << lfile.after(' ').trim_space()
2019-12-27 19:08:44 +01:00
}
}
return dir
}
// get retrieves `file` from the remote host.
2022-04-15 14:35:35 +02:00
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')
2019-12-27 19:08:44 +01:00
}
if code != ftp.open_data_connection {
return error('Data connection not ready')
2019-12-27 19:08:44 +01:00
}
blob := dtp.read()?
2019-12-27 19:08:44 +01:00
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
}