2020-11-15 23:54:47 +03:00
|
|
|
module ftp
|
|
|
|
|
2019-12-27 21:08:44 +03:00
|
|
|
/*
|
2020-05-16 17:12:23 +03:00
|
|
|
basic ftp module
|
2019-12-27 21:08:44 +03:00
|
|
|
RFC-959
|
|
|
|
https://tools.ietf.org/html/rfc959
|
|
|
|
|
|
|
|
Methods:
|
|
|
|
ftp.connect(host)
|
2019-12-28 10:53:28 +03:00
|
|
|
ftp.login(user, passw)
|
2019-12-27 21:08:44 +03:00
|
|
|
pwd := ftp.pwd()
|
|
|
|
ftp.cd(folder)
|
|
|
|
dtp := ftp.pasv()
|
|
|
|
ftp.dir()
|
|
|
|
ftp.get(file)
|
|
|
|
dtp.read()
|
|
|
|
dtp.close()
|
|
|
|
ftp.close()
|
|
|
|
*/
|
|
|
|
import net
|
2020-11-15 23:54:47 +03:00
|
|
|
import io
|
2019-12-27 21:08:44 +03:00
|
|
|
|
|
|
|
const (
|
2020-05-23 18:30:28 +03:00
|
|
|
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 21:08:44 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
struct DTP {
|
|
|
|
mut:
|
2022-09-15 07:59:31 +03:00
|
|
|
conn &net.TcpConn = unsafe { nil }
|
2020-11-15 23:54:47 +03:00
|
|
|
reader io.BufferedReader
|
2021-01-06 20:53:25 +03:00
|
|
|
ip string
|
|
|
|
port int
|
2019-12-27 21:08:44 +03:00
|
|
|
}
|
|
|
|
|
2022-10-16 09:28:57 +03:00
|
|
|
fn (mut dtp DTP) read() ![]u8 {
|
2022-04-15 15:35:35 +03:00
|
|
|
mut data := []u8{}
|
|
|
|
mut buf := []u8{len: 1024}
|
2021-01-07 22:21:47 +03:00
|
|
|
for {
|
|
|
|
len := dtp.reader.read(mut buf) or { break }
|
|
|
|
if len == 0 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
data << buf[..len]
|
|
|
|
}
|
2019-12-27 21:08:44 +03:00
|
|
|
return data
|
|
|
|
}
|
|
|
|
|
2021-01-20 13:11:01 +03:00
|
|
|
fn (mut dtp DTP) close() {
|
2021-03-01 02:18:14 +03:00
|
|
|
dtp.conn.close() or { panic(err) }
|
2019-12-27 21:08:44 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
struct FTP {
|
|
|
|
mut:
|
2022-09-15 07:59:31 +03:00
|
|
|
conn &net.TcpConn = unsafe { nil }
|
2021-01-06 20:53:25 +03:00
|
|
|
reader io.BufferedReader
|
2019-12-27 21:08:44 +03:00
|
|
|
buffer_size int
|
|
|
|
}
|
|
|
|
|
2022-05-16 08:52:12 +03:00
|
|
|
// new returns an `FTP` instance.
|
2019-12-27 21:08:44 +03:00
|
|
|
pub fn new() FTP {
|
2021-02-02 10:22:52 +03:00
|
|
|
mut f := FTP{
|
|
|
|
conn: 0
|
|
|
|
}
|
2019-12-27 21:08:44 +03:00
|
|
|
f.buffer_size = 1024
|
|
|
|
return f
|
|
|
|
}
|
|
|
|
|
2022-10-16 09:28:57 +03:00
|
|
|
fn (mut zftp FTP) write(data string) !int {
|
2019-12-27 21:08:44 +03:00
|
|
|
$if debug {
|
2022-11-15 16:53:13 +03:00
|
|
|
println('FTP.v >>> ${data}')
|
2019-12-27 21:08:44 +03:00
|
|
|
}
|
2022-11-15 16:53:13 +03:00
|
|
|
return zftp.conn.write('${data}\r\n'.bytes())
|
2019-12-27 21:08:44 +03:00
|
|
|
}
|
|
|
|
|
2022-10-16 09:28:57 +03:00
|
|
|
fn (mut zftp FTP) read() !(int, string) {
|
|
|
|
mut data := zftp.reader.read_line()!
|
2019-12-27 21:08:44 +03:00
|
|
|
$if debug {
|
2022-11-15 16:53:13 +03:00
|
|
|
println('FTP.v <<< ${data}')
|
2019-12-27 21:08:44 +03:00
|
|
|
}
|
|
|
|
if data.len < 5 {
|
2019-12-28 10:53:28 +03:00
|
|
|
return 0, ''
|
2019-12-27 21:08:44 +03:00
|
|
|
}
|
2020-01-19 15:53:13 +03:00
|
|
|
code := data[..3].int()
|
|
|
|
if data[3] == `-` {
|
2019-12-27 21:08:44 +03:00
|
|
|
for {
|
2022-10-16 09:28:57 +03:00
|
|
|
data = zftp.reader.read_line()!
|
2020-01-19 15:53:13 +03:00
|
|
|
if data[..3].int() == code && data[3] != `-` {
|
2019-12-27 21:08:44 +03:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-12-28 10:53:28 +03:00
|
|
|
return code, data
|
2019-12-27 21:08:44 +03:00
|
|
|
}
|
|
|
|
|
2022-05-16 08:52:12 +03:00
|
|
|
// connect establishes an FTP connection to the host at `ip` port 21.
|
2022-10-16 09:28:57 +03:00
|
|
|
pub fn (mut zftp FTP) connect(ip string) !bool {
|
2022-11-15 16:53:13 +03:00
|
|
|
zftp.conn = net.dial_tcp('${ip}:21')!
|
2021-05-13 10:26:10 +03:00
|
|
|
zftp.reader = io.new_buffered_reader(reader: zftp.conn)
|
2022-10-16 09:28:57 +03:00
|
|
|
code, _ := zftp.read()!
|
2021-01-26 17:43:10 +03:00
|
|
|
if code == ftp.connected {
|
2019-12-27 21:08:44 +03:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2022-05-16 08:52:12 +03:00
|
|
|
// login sends the "USER `user`" and "PASS `passwd`" commands to the remote host.
|
2022-10-16 09:28:57 +03:00
|
|
|
pub fn (mut zftp FTP) login(user string, passwd string) !bool {
|
2022-11-15 16:53:13 +03:00
|
|
|
zftp.write('USER ${user}') or {
|
2019-12-28 10:53:28 +03:00
|
|
|
$if debug {
|
|
|
|
println('ERROR sending user')
|
|
|
|
}
|
2019-12-27 21:08:44 +03:00
|
|
|
return false
|
|
|
|
}
|
2022-10-16 09:28:57 +03:00
|
|
|
mut code, _ := zftp.read()!
|
2021-01-26 17:43:10 +03:00
|
|
|
if code == ftp.logged_in {
|
2019-12-27 21:08:44 +03:00
|
|
|
return true
|
|
|
|
}
|
2021-01-26 17:43:10 +03:00
|
|
|
if code != ftp.specify_password {
|
2019-12-27 21:08:44 +03:00
|
|
|
return false
|
|
|
|
}
|
2022-11-15 16:53:13 +03:00
|
|
|
zftp.write('PASS ${passwd}') or {
|
2019-12-28 10:53:28 +03:00
|
|
|
$if debug {
|
|
|
|
println('ERROR sending password')
|
|
|
|
}
|
2019-12-27 21:08:44 +03:00
|
|
|
return false
|
|
|
|
}
|
2022-10-16 09:28:57 +03:00
|
|
|
code, _ = zftp.read()!
|
2021-01-26 17:43:10 +03:00
|
|
|
if code == ftp.logged_in {
|
2019-12-27 21:08:44 +03:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2022-05-16 08:52:12 +03:00
|
|
|
// close closes the FTP connection.
|
2022-10-16 09:28:57 +03:00
|
|
|
pub fn (mut zftp FTP) close() ! {
|
|
|
|
zftp.write('QUIT')!
|
|
|
|
zftp.conn.close()!
|
2019-12-27 21:08:44 +03:00
|
|
|
}
|
|
|
|
|
2022-05-16 08:52:12 +03:00
|
|
|
// pwd returns the current working directory on the remote host for the logged in user.
|
2022-10-16 09:28:57 +03:00
|
|
|
pub fn (mut zftp FTP) pwd() !string {
|
|
|
|
zftp.write('PWD')!
|
|
|
|
_, data := zftp.read()!
|
2020-08-10 19:05:26 +03:00
|
|
|
spl := data.split('"') // "
|
2019-12-27 21:08:44 +03:00
|
|
|
if spl.len >= 2 {
|
|
|
|
return spl[1]
|
|
|
|
}
|
|
|
|
return data
|
|
|
|
}
|
|
|
|
|
2022-05-16 08:52:12 +03:00
|
|
|
// cd changes the current working directory to the specified remote directory `dir`.
|
2022-10-16 09:28:57 +03:00
|
|
|
pub fn (mut zftp FTP) cd(dir string) ! {
|
2022-11-15 16:53:13 +03:00
|
|
|
zftp.write('CWD ${dir}') or { return }
|
2022-10-16 09:28:57 +03:00
|
|
|
mut code, mut data := zftp.read()!
|
2020-05-04 22:56:41 +03:00
|
|
|
match int(code) {
|
2021-01-26 17:43:10 +03:00
|
|
|
ftp.denied {
|
2019-12-28 10:53:28 +03:00
|
|
|
$if debug {
|
2022-11-15 16:53:13 +03:00
|
|
|
println('CD ${dir} denied!')
|
2019-12-28 10:53:28 +03:00
|
|
|
}
|
2019-12-27 21:08:44 +03:00
|
|
|
}
|
2021-01-26 17:43:10 +03:00
|
|
|
ftp.complete {
|
2022-10-16 09:28:57 +03:00
|
|
|
code, data = zftp.read()!
|
2019-12-27 21:08:44 +03:00
|
|
|
}
|
|
|
|
else {}
|
|
|
|
}
|
2019-12-28 10:53:28 +03:00
|
|
|
$if debug {
|
2022-11-15 16:53:13 +03:00
|
|
|
println('CD ${data}')
|
2019-12-28 10:53:28 +03:00
|
|
|
}
|
2019-12-27 21:08:44 +03:00
|
|
|
}
|
|
|
|
|
2022-10-16 09:28:57 +03:00
|
|
|
fn new_dtp(msg string) !&DTP {
|
2019-12-28 10:53:28 +03:00
|
|
|
if !is_dtp_message_valid(msg) {
|
|
|
|
return error('Bad message')
|
2019-12-27 21:08:44 +03:00
|
|
|
}
|
2019-12-28 10:53:28 +03:00
|
|
|
ip, port := get_host_ip_from_dtp_message(msg)
|
2021-01-07 22:21:47 +03:00
|
|
|
mut dtp := &DTP{
|
2019-12-27 21:08:44 +03:00
|
|
|
ip: ip
|
|
|
|
port: port
|
2021-02-02 10:22:52 +03:00
|
|
|
conn: 0
|
2019-12-27 21:08:44 +03:00
|
|
|
}
|
2022-11-15 16:53:13 +03:00
|
|
|
conn := net.dial_tcp('${ip}:${port}') or { return error('Cannot connect to the data channel') }
|
2021-01-07 22:21:47 +03:00
|
|
|
dtp.conn = conn
|
2021-05-13 10:26:10 +03:00
|
|
|
dtp.reader = io.new_buffered_reader(reader: dtp.conn)
|
2019-12-27 21:08:44 +03:00
|
|
|
return dtp
|
|
|
|
}
|
|
|
|
|
2022-10-16 09:28:57 +03:00
|
|
|
fn (mut zftp FTP) pasv() !&DTP {
|
|
|
|
zftp.write('PASV')!
|
|
|
|
code, data := zftp.read()!
|
2019-12-28 10:53:28 +03:00
|
|
|
$if debug {
|
2022-11-15 16:53:13 +03:00
|
|
|
println('pass: ${data}')
|
2019-12-28 10:53:28 +03:00
|
|
|
}
|
2021-01-26 17:43:10 +03:00
|
|
|
if code != ftp.passive_mode {
|
2023-05-12 09:31:27 +03:00
|
|
|
return error('passive mode not allowed')
|
2019-12-27 21:08:44 +03:00
|
|
|
}
|
2022-10-16 09:28:57 +03:00
|
|
|
dtp := new_dtp(data)!
|
2019-12-27 21:08:44 +03:00
|
|
|
return dtp
|
|
|
|
}
|
|
|
|
|
2022-05-16 08:52:12 +03:00
|
|
|
// dir returns a list of the files in the current working directory.
|
2022-10-16 09:28:57 +03:00
|
|
|
pub fn (mut zftp FTP) dir() ![]string {
|
2021-01-26 17:43:10 +03:00
|
|
|
mut dtp := zftp.pasv() or { return error('Cannot establish data connection') }
|
2022-10-16 09:28:57 +03:00
|
|
|
zftp.write('LIST')!
|
|
|
|
code, _ := zftp.read()!
|
2021-01-26 17:43:10 +03:00
|
|
|
if code == ftp.denied {
|
2021-01-06 20:53:25 +03:00
|
|
|
return error('`LIST` denied')
|
2019-12-27 21:08:44 +03:00
|
|
|
}
|
2021-01-26 17:43:10 +03:00
|
|
|
if code != ftp.open_data_connection {
|
2021-01-06 20:53:25 +03:00
|
|
|
return error('Data channel empty')
|
2019-12-27 21:08:44 +03:00
|
|
|
}
|
2022-10-16 09:28:57 +03:00
|
|
|
list_dir := dtp.read()!
|
|
|
|
result, _ := zftp.read()!
|
2021-01-26 17:43:10 +03:00
|
|
|
if result != ftp.close_data_connection {
|
2021-01-06 20:53:25 +03:00
|
|
|
println('`LIST` not ok')
|
2019-12-27 21:08:44 +03:00
|
|
|
}
|
|
|
|
dtp.close()
|
2020-04-26 14:49:31 +03:00
|
|
|
mut dir := []string{}
|
2020-08-10 19:05:26 +03:00
|
|
|
sdir := list_dir.bytestr()
|
2019-12-27 21:08:44 +03:00
|
|
|
for lfile in sdir.split('\n') {
|
2023-07-07 06:50:20 +03:00
|
|
|
if lfile.len > 56 {
|
|
|
|
dir << lfile#[56..lfile.len - 1]
|
|
|
|
continue
|
|
|
|
}
|
2020-05-16 17:12:23 +03:00
|
|
|
if lfile.len > 1 {
|
2023-07-07 06:50:20 +03:00
|
|
|
trimmed := lfile.after(':')
|
|
|
|
dir << trimmed#[3..trimmed.len - 1]
|
|
|
|
continue
|
2019-12-27 21:08:44 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return dir
|
|
|
|
}
|
|
|
|
|
2022-05-16 08:52:12 +03:00
|
|
|
// get retrieves `file` from the remote host.
|
2022-10-16 09:28:57 +03:00
|
|
|
pub fn (mut zftp FTP) get(file string) ![]u8 {
|
2021-01-26 17:43:10 +03:00
|
|
|
mut dtp := zftp.pasv() or { return error('Cannot stablish data connection') }
|
2022-11-15 16:53:13 +03:00
|
|
|
zftp.write('RETR ${file}')!
|
2022-10-16 09:28:57 +03:00
|
|
|
code, _ := zftp.read()!
|
2021-01-26 17:43:10 +03:00
|
|
|
if code == ftp.denied {
|
2019-12-28 10:53:28 +03:00
|
|
|
return error('Permission denied')
|
2019-12-27 21:08:44 +03:00
|
|
|
}
|
2021-01-26 17:43:10 +03:00
|
|
|
if code != ftp.open_data_connection {
|
2019-12-28 10:53:28 +03:00
|
|
|
return error('Data connection not ready')
|
2019-12-27 21:08:44 +03:00
|
|
|
}
|
2022-10-16 09:28:57 +03:00
|
|
|
blob := dtp.read()!
|
2019-12-27 21:08:44 +03:00
|
|
|
dtp.close()
|
|
|
|
return blob
|
|
|
|
}
|
2019-12-28 10:53:28 +03:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|