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)
|
2019-12-28 08:53:28 +01:00
|
|
|
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()
|
|
|
|
*/
|
|
|
|
module ftp
|
|
|
|
|
|
|
|
import net
|
|
|
|
|
|
|
|
const (
|
2020-05-23 17:30:28 +02: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 19:08:44 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
struct DTP {
|
|
|
|
mut:
|
|
|
|
sock net.Socket
|
2020-05-16 16:12:23 +02:00
|
|
|
ip string
|
2019-12-27 19:08:44 +01:00
|
|
|
port int
|
|
|
|
}
|
|
|
|
|
|
|
|
fn (dtp DTP) read() []byte {
|
2020-04-26 13:49:31 +02:00
|
|
|
mut data := []byte{}
|
2019-12-27 19:08:44 +01:00
|
|
|
for {
|
2019-12-28 08:53:28 +01:00
|
|
|
buf, len := dtp.sock.recv(1024)
|
2020-05-16 16:12:23 +02:00
|
|
|
if len == 0 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
for i in 0 .. len {
|
2019-12-27 19:08:44 +01:00
|
|
|
data << buf[i]
|
|
|
|
}
|
2020-05-16 16:12:23 +02:00
|
|
|
unsafe {
|
|
|
|
free(buf)
|
|
|
|
}
|
2019-12-27 19:08:44 +01:00
|
|
|
}
|
|
|
|
return data
|
|
|
|
}
|
|
|
|
|
|
|
|
fn (dtp DTP) close() {
|
2020-05-16 16:12:23 +02:00
|
|
|
dtp.sock.close() or {
|
|
|
|
}
|
2019-12-27 19:08:44 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
struct FTP {
|
|
|
|
mut:
|
2020-05-16 16:12:23 +02:00
|
|
|
sock net.Socket
|
2019-12-27 19:08:44 +01:00
|
|
|
buffer_size int
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn new() FTP {
|
|
|
|
mut f := FTP{}
|
|
|
|
f.buffer_size = 1024
|
|
|
|
return f
|
|
|
|
}
|
|
|
|
|
|
|
|
fn (ftp FTP) write(data string) ?int {
|
|
|
|
$if debug {
|
|
|
|
println('FTP.v >>> $data')
|
|
|
|
}
|
2020-01-19 13:53:13 +01:00
|
|
|
n := ftp.sock.send_string('$data\r\n') or {
|
2019-12-28 08:53:28 +01:00
|
|
|
return error('Cannot send data')
|
2019-12-27 19:08:44 +01:00
|
|
|
}
|
|
|
|
return n
|
|
|
|
}
|
|
|
|
|
2019-12-28 08:53:28 +01:00
|
|
|
fn (ftp FTP) read() (int, string) {
|
2019-12-27 19:08:44 +01:00
|
|
|
mut data := ftp.sock.read_line()
|
|
|
|
$if debug {
|
|
|
|
println('FTP.v <<< $data')
|
|
|
|
}
|
|
|
|
if data.len < 5 {
|
2019-12-28 08:53:28 +01:00
|
|
|
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 = ftp.sock.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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-12-28 08:53:28 +01:00
|
|
|
return code, data
|
2019-12-27 19:08:44 +01:00
|
|
|
}
|
|
|
|
|
2020-05-16 16:12:23 +02:00
|
|
|
pub fn (mut ftp FTP) connect(ip string) bool {
|
2019-12-27 19:08:44 +01:00
|
|
|
sock := net.dial(ip, 21) or {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
ftp.sock = sock
|
2019-12-28 08:53:28 +01:00
|
|
|
code, _ := ftp.read()
|
2020-05-23 17:30:28 +02:00
|
|
|
if code == connected {
|
2019-12-27 19:08:44 +01:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn (ftp FTP) login(user, passwd string) bool {
|
2019-12-28 08:53:28 +01:00
|
|
|
ftp.write('USER $user') or {
|
|
|
|
$if debug {
|
|
|
|
println('ERROR sending user')
|
|
|
|
}
|
2019-12-27 19:08:44 +01:00
|
|
|
return false
|
|
|
|
}
|
2020-05-23 17:30:28 +02:00
|
|
|
mut code, mut data := ftp.read()
|
|
|
|
if code == logged_in {
|
2019-12-27 19:08:44 +01:00
|
|
|
return true
|
|
|
|
}
|
2020-05-23 17:30:28 +02:00
|
|
|
if code != specify_password {
|
2019-12-27 19:08:44 +01:00
|
|
|
return false
|
|
|
|
}
|
2019-12-28 08:53:28 +01:00
|
|
|
ftp.write('PASS $passwd') or {
|
|
|
|
$if debug {
|
|
|
|
println('ERROR sending password')
|
|
|
|
}
|
2019-12-27 19:08:44 +01:00
|
|
|
return false
|
|
|
|
}
|
2019-12-28 08:53:28 +01:00
|
|
|
code, data = ftp.read()
|
2020-05-16 16:12:23 +02:00
|
|
|
// TODO Replace `data` with `_`
|
|
|
|
_ := data
|
2020-05-23 17:30:28 +02:00
|
|
|
if code == logged_in {
|
2019-12-27 19:08:44 +01:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn (ftp FTP) close() {
|
|
|
|
send_quit := 'QUIT\r\n'
|
2020-05-16 16:12:23 +02:00
|
|
|
ftp.sock.send_string(send_quit) or {
|
|
|
|
}
|
|
|
|
ftp.sock.close() or {
|
|
|
|
}
|
2019-12-27 19:08:44 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn (ftp FTP) pwd() string {
|
|
|
|
ftp.write('PWD') or {
|
|
|
|
return ''
|
|
|
|
}
|
2019-12-28 08:53:28 +01:00
|
|
|
_, data := ftp.read()
|
2019-12-27 19:08:44 +01:00
|
|
|
spl := data.split('"')
|
|
|
|
if spl.len >= 2 {
|
|
|
|
return spl[1]
|
|
|
|
}
|
|
|
|
return data
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn (ftp FTP) cd(dir string) {
|
2020-05-16 16:12:23 +02:00
|
|
|
ftp.write('CWD $dir') or {
|
|
|
|
return
|
|
|
|
}
|
2019-12-27 19:08:44 +01:00
|
|
|
mut code, mut data := ftp.read()
|
2020-05-04 21:56:41 +02:00
|
|
|
match int(code) {
|
|
|
|
denied {
|
2019-12-28 08:53:28 +01:00
|
|
|
$if debug {
|
|
|
|
println('CD $dir denied!')
|
|
|
|
}
|
2019-12-27 19:08:44 +01:00
|
|
|
}
|
2020-05-04 21:56:41 +02:00
|
|
|
complete {
|
2019-12-28 08:53:28 +01:00
|
|
|
code, data = ftp.read()
|
2019-12-27 19:08:44 +01:00
|
|
|
}
|
|
|
|
else {}
|
|
|
|
}
|
2019-12-28 08:53:28 +01:00
|
|
|
$if debug {
|
|
|
|
println('CD $data')
|
|
|
|
}
|
2019-12-27 19:08:44 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
fn new_dtp(msg string) ?DTP {
|
2019-12-28 08:53:28 +01:00
|
|
|
if !is_dtp_message_valid(msg) {
|
|
|
|
return error('Bad message')
|
2019-12-27 19:08:44 +01:00
|
|
|
}
|
2019-12-28 08:53:28 +01:00
|
|
|
ip, port := get_host_ip_from_dtp_message(msg)
|
2019-12-27 19:08:44 +01:00
|
|
|
sock := net.dial(ip, port) or {
|
2019-12-28 08:53:28 +01:00
|
|
|
return error('Cannot connect to the data channel')
|
2019-12-27 19:08:44 +01:00
|
|
|
}
|
2020-05-16 16:12:23 +02:00
|
|
|
dtp := DTP{
|
2019-12-28 08:53:28 +01:00
|
|
|
sock: sock
|
2019-12-27 19:08:44 +01:00
|
|
|
ip: ip
|
|
|
|
port: port
|
|
|
|
}
|
|
|
|
return dtp
|
|
|
|
}
|
|
|
|
|
|
|
|
fn (ftp FTP) pasv() ?DTP {
|
2020-05-16 16:12:23 +02:00
|
|
|
ftp.write('PASV') or {
|
|
|
|
}
|
2019-12-28 08:53:28 +01:00
|
|
|
code, data := ftp.read()
|
|
|
|
$if debug {
|
|
|
|
println('pass: $data')
|
|
|
|
}
|
2020-05-23 17:30:28 +02:00
|
|
|
if code != passive_mode {
|
2019-12-27 19:08:44 +01:00
|
|
|
return error('pasive mode not allowed')
|
|
|
|
}
|
2020-04-10 21:52:12 +02:00
|
|
|
dtp := new_dtp(data) or {
|
2020-05-16 16:12:23 +02:00
|
|
|
return error(err)
|
2020-04-10 21:52:12 +02:00
|
|
|
}
|
2019-12-27 19:08:44 +01:00
|
|
|
return dtp
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn (ftp FTP) dir() ?[]string {
|
|
|
|
dtp := ftp.pasv() or {
|
|
|
|
return error('cannot establish data connection')
|
|
|
|
}
|
2020-05-16 16:12:23 +02:00
|
|
|
ftp.write('LIST') or {
|
|
|
|
}
|
2019-12-28 08:53:28 +01:00
|
|
|
code, _ := ftp.read()
|
2020-05-04 21:56:41 +02:00
|
|
|
if code == denied {
|
2019-12-28 08:53:28 +01:00
|
|
|
return error('LIST denied')
|
2019-12-27 19:08:44 +01:00
|
|
|
}
|
2020-05-23 17:30:28 +02:00
|
|
|
if code != open_data_connection {
|
2019-12-27 19:08:44 +01:00
|
|
|
return error('data channel empty')
|
|
|
|
}
|
|
|
|
list_dir := dtp.read()
|
2019-12-28 08:53:28 +01:00
|
|
|
result, _ := ftp.read()
|
2020-05-23 17:30:28 +02:00
|
|
|
if result != close_data_connection {
|
2019-12-27 19:08:44 +01:00
|
|
|
println('LIST not ok')
|
|
|
|
}
|
|
|
|
dtp.close()
|
2020-04-26 13:49:31 +02:00
|
|
|
mut dir := []string{}
|
2019-12-27 19:08:44 +01:00
|
|
|
sdir := string(byteptr(list_dir.data))
|
|
|
|
for lfile in sdir.split('\n') {
|
2020-05-16 16:12:23 +02:00
|
|
|
if lfile.len > 1 {
|
2019-12-27 19:08:44 +01:00
|
|
|
spl := lfile.split(' ')
|
2020-05-16 16:12:23 +02:00
|
|
|
dir << spl[spl.len - 1]
|
2019-12-27 19:08:44 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return dir
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn (ftp FTP) get(file string) ?[]byte {
|
|
|
|
dtp := ftp.pasv() or {
|
2019-12-28 08:53:28 +01:00
|
|
|
return error('Cannot stablish data connection')
|
2019-12-27 19:08:44 +01:00
|
|
|
}
|
2020-05-16 16:12:23 +02:00
|
|
|
ftp.write('RETR $file') or {
|
|
|
|
}
|
2019-12-28 08:53:28 +01:00
|
|
|
code, _ := ftp.read()
|
2020-05-04 21:56:41 +02:00
|
|
|
if code == denied {
|
2019-12-28 08:53:28 +01:00
|
|
|
return error('Permission denied')
|
2019-12-27 19:08:44 +01:00
|
|
|
}
|
2020-05-23 17:30:28 +02:00
|
|
|
if code != open_data_connection {
|
2019-12-28 08:53:28 +01:00
|
|
|
return error('Data connection not ready')
|
2019-12-27 19:08:44 +01:00
|
|
|
}
|
|
|
|
blob := dtp.read()
|
|
|
|
dtp.close()
|
|
|
|
return blob
|
|
|
|
}
|
2019-12-28 08:53:28 +01: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
|
|
|
|
}
|