1/* ftpget.c - Fetch file(s) from ftp server
2 *
3 * Copyright 2016 Rob Landley <rob@landley.net>
4 *
5 * No standard for the command, but see https://www.ietf.org/rfc/rfc959.txt
6 * TODO: local can be -
7 * TEST: -g -s (when local and remote exist) -gc, -sc
8 * zero length file
9
10USE_FTPGET(NEWTOY(ftpget, "<2>3P:cp:u:vgslLmMdD[-gs][!gslLmMdD][!clL]", TOYFLAG_USR|TOYFLAG_BIN))
11USE_FTPPUT(OLDTOY(ftpput, ftpget, TOYFLAG_USR|TOYFLAG_BIN))
12
13config FTPGET
14  bool "ftpget"
15  default y
16  help
17    usage: ftpget [-cvgslLmMdD] [-P PORT] [-p PASSWORD] [-u USER] HOST [LOCAL] REMOTE
18
19    Talk to ftp server. By default get REMOTE file via passive anonymous
20    transfer, optionally saving under a LOCAL name. Can also send, list, etc.
21
22    -c	Continue partial transfer
23    -p	Use PORT instead of "21"
24    -P	Use PASSWORD instead of "ftpget@"
25    -u	Use USER instead of "anonymous"
26    -v	Verbose
27
28    Ways to interact with FTP server:
29    -d	Delete file
30    -D	Remove directory
31    -g	Get file (default)
32    -l	List directory
33    -L	List (filenames only)
34    -m	Move file on server from LOCAL to REMOTE
35    -M	mkdir
36    -s	Send file
37
38config FTPPUT
39  bool "ftpput"
40  default y
41  help
42    An ftpget that defaults to -s instead of -g
43*/
44
45#define FOR_ftpget
46#include "toys.h"
47
48GLOBALS(
49  char *user;
50  char *port;
51  char *password;
52
53  int fd;
54)
55
56// we should get one line of data, but it may be in multiple chunks
57static int xread2line(int fd, char *buf, int len)
58{
59  int i, total = 0;
60
61  len--;
62  while (total<len && (i = xread(fd, buf, len-total))) {
63    total += i;
64    if (buf[total-1] == '\n') break;
65  }
66  if (total>=len) error_exit("overflow");
67  while (total--)
68    if (buf[total]=='\r' || buf[total]=='\n') buf[total] = 0;
69    else break;
70  if (toys.optflags & FLAG_v) fprintf(stderr, "%s\n", toybuf);
71
72  return total+1;
73}
74
75static int ftp_line(char *cmd, char *arg, int must)
76{
77  int rc = 0;
78
79  if (cmd) {
80    char *s = "%s %s\r\n"+3*(!arg);
81    if (toys.optflags & FLAG_v) fprintf(stderr, s, cmd, arg);
82    dprintf(TT.fd, s, cmd, arg);
83  }
84  if (must>=0) {
85    xread2line(TT.fd, toybuf, sizeof(toybuf));
86    if (!sscanf(toybuf, "%d", &rc) || (must && rc != must))
87      error_exit_raw(toybuf);
88  }
89
90  return rc;
91}
92
93void ftpget_main(void)
94{
95  struct sockaddr_in6 si6;
96  int rc, ii = 1, port;
97  socklen_t sl = sizeof(si6);
98  char *s, *remote = toys.optargs[2];
99  unsigned long long lenl = 0, lenr;
100
101  if (!(toys.optflags&(FLAG_v-1)))
102    toys.optflags |= (toys.which->name[3]=='g') ? FLAG_g : FLAG_s;
103
104  if (!TT.user) TT.user = "anonymous";
105  if (!TT.password) TT.password = "ftpget@";
106  if (!TT.port) TT.port = "21";
107  if (!remote) remote = toys.optargs[1];
108
109  // connect
110  TT.fd = xconnect(*toys.optargs, TT.port, 0, SOCK_STREAM, 0, AI_ADDRCONFIG);
111  if (getpeername(TT.fd, (void *)&si6, &sl)) perror_exit("getpeername");
112
113  // Login
114  ftp_line(0, 0, 220);
115  rc = ftp_line("USER", TT.user, 0);
116  if (rc == 331) rc = ftp_line("PASS", TT.password, 0);
117  if (rc != 230) error_exit_raw(toybuf);
118
119  if (toys.optflags & FLAG_m) {
120    if (toys.optc != 3) error_exit("-m FROM TO");
121    ftp_line("RNFR", toys.optargs[1], 350);
122    ftp_line("RNTO", toys.optargs[2], 250);
123  } else if (toys.optflags & FLAG_M) ftp_line("MKD", toys.optargs[1], 257);
124  else if (toys.optflags & FLAG_d) ftp_line("DELE", toys.optargs[1], 250);
125  else if (toys.optflags & FLAG_D) ftp_line("RMD", toys.optargs[1], 250);
126  else {
127    int get = !(toys.optflags&FLAG_s), cnt = toys.optflags&FLAG_c;
128    char *cmd;
129
130    // Only do passive binary transfers
131    ftp_line("TYPE", "I", 0);
132    rc = ftp_line("PASV", 0, 0);
133
134    // PASV means the server opens a port you connect to instead of the server
135    // dialing back to the client. (Still insane, but less so.) So need port #
136
137    // PASV output is "227 PASV ok (x,x,x,x,p1,p2)" where x,x,x,x is the IP addr
138    // (must match the server you're talking to???) and port is (256*p1)+p2
139    s = 0;
140    if (rc==227) for (s = toybuf; (s = strchr(s, ',')); s++) {
141      int p1, got = 0;
142
143      sscanf(s, ",%u,%u)%n", &p1, &port, &got);
144      if (!got) continue;
145      port += 256*p1;
146      break;
147    }
148    if (!s || port<1 || port>65535) error_exit_raw(toybuf);
149    si6.sin6_port = SWAP_BE16(port); // same field size/offset for v4 and v6
150    port = xsocket(si6.sin6_family, SOCK_STREAM, 0);
151    if (connect(port, (void *)&si6, sizeof(si6))) perror_exit("connect");
152
153    // RETR blocks until file data read from data port, so use SIZE to check
154    // if file exists before creating local copy
155    lenr = 0;
156    if (toys.optflags&(FLAG_s|FLAG_g)) {
157      if (ftp_line("SIZE", remote, 0) == 213)
158        sscanf(toybuf, "%*u %llu", &lenr);
159      else if (get) error_exit("no %s", remote);
160    }
161
162    // Open file for reading or writing
163    if (toys.optflags & (FLAG_g|FLAG_s)) {
164      if (strcmp(toys.optargs[1], "-"))
165        ii = xcreate(toys.optargs[1],
166          get ? (cnt ? O_APPEND : O_TRUNC)|O_CREAT|O_WRONLY : O_RDONLY, 0666);
167      lenl = fdlength(ii);
168    }
169    if (get) {
170      cmd = "REST";
171      if (toys.optflags&FLAG_l) cmd = "LIST";
172      if (toys.optflags&FLAG_L) cmd = "NLST";
173      if (cnt) {
174        char buf[32];
175
176        if (lenl>=lenr) goto done;
177        sprintf(buf, "%llu", lenl);
178        ftp_line("REST", buf, 350);
179      } else lenl = 0;
180
181      ftp_line(cmd, remote, -1);
182      lenl += xsendfile(port, ii);
183      ftp_line(0, 0, (toys.optflags&FLAG_g) ? 226 : 150);
184    } else if (toys.optflags & FLAG_s) {
185      cmd = "STOR";
186      if (cnt && lenr) {
187        cmd = "APPE";
188        xlseek(ii, lenl, SEEK_SET);
189      } else lenr = 0;
190      ftp_line(cmd, remote, 150);
191      lenr += xsendfile(ii, port);
192      close(port);
193    }
194    if (toys.optflags&(FLAG_g|FLAG_s))
195      if (lenl != lenr) error_exit("short %lld/%lld", lenl, lenr);
196  }
197  ftp_line("QUIT", 0, 0);
198
199done:
200  if (CFG_TOYBOX_FREE) {
201    if (ii!=1) xclose(ii);
202    xclose(port);
203    xclose(TT.fd);
204  }
205}
206