ftpgetput: deal with long-standing TODOs:
[oweals/busybox.git] / networking / ftpgetput.c
1 /* vi: set sw=4 ts=4: */
2 /*
3  * ftpget
4  *
5  * Mini implementation of FTP to retrieve a remote file.
6  *
7  * Copyright (C) 2002 Jeff Angielski, The PTR Group <jeff@theptrgroup.com>
8  * Copyright (C) 2002 Glenn McGrath
9  *
10  * Based on wget.c by Chip Rosenthal Covad Communications
11  * <chip@laserlink.net>
12  *
13  * Licensed under GPLv2 or later, see file LICENSE in this tarball for details.
14  */
15
16 #include "libbb.h"
17
18 struct globals {
19         const char *user;
20         const char *password;
21         struct len_and_sockaddr *lsa;
22         int verbose_flag;
23         int do_continue;
24         char buf[1]; /* actually [BUF_SIZE] */
25 };
26 #define G (*(struct globals*)&bb_common_bufsiz1)
27 enum { BUFSZ = COMMON_BUFSIZE - offsetof(struct globals, buf) };
28 struct BUG_G_too_big {
29         char BUG_G_too_big[sizeof(G) <= COMMON_BUFSIZE ? 1 : -1];
30 };
31 #define user         (G.user        )
32 #define password     (G.password    )
33 #define lsa          (G.lsa         )
34 #define verbose_flag (G.verbose_flag)
35 #define do_continue  (G.do_continue )
36 #define buf          (G.buf         )
37 #define INIT_G() do { \
38 } while (0)
39
40
41 static void ftp_die(const char *msg) ATTRIBUTE_NORETURN;
42 static void ftp_die(const char *msg)
43 {
44         const char *cp = buf; /* buf holds peer's response */
45
46         /* Guard against garbage from remote server */
47         while (*cp >= ' ' && *cp < '\x7f')
48                 cp++;
49         bb_error_msg_and_die("unexpected server response%s%s: %.*s",
50                         msg ? " to " : "", msg ? msg : "",
51                         (int)(cp - buf), buf);
52 }
53
54 static int ftpcmd(const char *s1, const char *s2, FILE *stream)
55 {
56         unsigned n;
57         if (verbose_flag) {
58                 bb_error_msg("cmd %s %s", s1, s2);
59         }
60
61         if (s1) {
62                 fprintf(stream, (s2 ? "%s %s\r\n" : "%s %s\r\n"+3), s1, s2);
63         }
64
65         do {
66                 char *buf_ptr;
67
68                 if (fgets(buf, BUFSZ - 2, stream) == NULL) {
69                         bb_perror_msg_and_die("fgets");
70                 }
71                 buf_ptr = strstr(buf, "\r\n");
72                 if (buf_ptr) {
73                         *buf_ptr = '\0';
74                 }
75         } while (!isdigit(buf[0]) || buf[3] != ' ');
76
77         buf[3] = '\0';
78         n = xatou(buf);
79         buf[3] = ' ';
80         return n;
81 }
82
83 static FILE *ftp_login(void)
84 {
85         FILE *control_stream;
86
87         /* Connect to the command socket */
88         control_stream = fdopen(xconnect_stream(lsa), "r+");
89         if (control_stream == NULL) {
90                 /* fdopen failed - extremely unlikely */
91                 bb_perror_nomsg_and_die();
92         }
93
94         if (ftpcmd(NULL, NULL, control_stream) != 220) {
95                 ftp_die(NULL);
96         }
97
98         /*  Login to the server */
99         switch (ftpcmd("USER", user, control_stream)) {
100         case 230:
101                 break;
102         case 331:
103                 if (ftpcmd("PASS", password, control_stream) != 230) {
104                         ftp_die("PASS");
105                 }
106                 break;
107         default:
108                 ftp_die("USER");
109         }
110
111         ftpcmd("TYPE I", NULL, control_stream);
112
113         return control_stream;
114 }
115
116 static int xconnect_ftpdata(void)
117 {
118         char *buf_ptr;
119         unsigned port_num;
120
121         /* Response is "NNN garbageN1,N2,N3,N4,P1,P2[)garbage]
122          * Server's IP is N1.N2.N3.N4 (we ignore it)
123          * Server's port for data connection is P1*256+P2 */
124         buf_ptr = strrchr(buf, ')');
125         if (buf_ptr) *buf_ptr = '\0';
126
127         buf_ptr = strrchr(buf, ',');
128         *buf_ptr = '\0';
129         port_num = xatoul_range(buf_ptr + 1, 0, 255);
130
131         buf_ptr = strrchr(buf, ',');
132         *buf_ptr = '\0';
133         port_num += xatoul_range(buf_ptr + 1, 0, 255) * 256;
134
135         set_nport(lsa, htons(port_num));
136         return xconnect_stream(lsa);
137 }
138
139 #if !ENABLE_FTPGET
140 int ftp_receive(FILE *control_stream,
141                 const char *local_path, char *server_path);
142 #else
143 static
144 int ftp_receive(FILE *control_stream,
145                 const char *local_path, char *server_path)
146 {
147 #define filesize ((off_t)-1)
148         int fd_data;
149         int fd_local = -1;
150         off_t beg_range = 0;
151
152 /*
153 TODO: PASV command will not work for IPv6. RFC2428 describes
154 IPv6-capable "extended PASV" - EPSV.
155
156 "EPSV [protocol]" asks server to bind to and listen on a data port
157 in specified protocol. Protocol is 1 for IPv4, 2 for IPv6.
158 If not specified, defaults to "same as used for control connection".
159 If server understood you, it should answer "229 <some text>(|||port|)"
160 where "|" are literal pipe chars and "port" is ASCII decimal port#.
161
162 There is also an IPv6-capable replacement for PORT (EPRT),
163 but we don't need that.
164
165 TODO: fold in sending of PASV/EPSV and parsing of response into
166 xconnect_ftpdata(). (Also, need to stop ignoring IP address in PASV
167 response).
168 */
169
170         /* connect to the data socket */
171         if (ftpcmd("PASV", NULL, control_stream) != 227) {
172                 ftp_die("PASV");
173         }
174         fd_data = xconnect_ftpdata();
175
176         if (ftpcmd("SIZE", server_path, control_stream) != 213) {
177                 do_continue = 0;
178         }
179
180         if (LONE_DASH(local_path)) {
181                 fd_local = STDOUT_FILENO;
182                 do_continue = 0;
183         }
184
185         if (do_continue) {
186                 struct stat sbuf;
187                 /* lstat would be wrong here! */
188                 if (stat(local_path, &sbuf) < 0) {
189                         bb_perror_msg_and_die("stat");
190                 }
191                 if (sbuf.st_size > 0) {
192                         beg_range = sbuf.st_size;
193                 } else {
194                         do_continue = 0;
195                 }
196         }
197
198         if (do_continue) {
199                 sprintf(buf, "REST %"OFF_FMT"d", beg_range);
200                 if (ftpcmd(buf, NULL, control_stream) != 350) {
201                         do_continue = 0;
202                 }
203         }
204
205         if (ftpcmd("RETR", server_path, control_stream) > 150) {
206                 ftp_die("RETR");
207         }
208
209         /* make local _after_ we know that remote file exists */
210         if (fd_local == -1) {
211                 fd_local = xopen(local_path,
212                         do_continue ? (O_APPEND | O_WRONLY)
213                                     : (O_CREAT | O_TRUNC | O_WRONLY)
214                 );
215         }
216
217 // TODO: merge tail of ftp_receive and ftp_send starting from here
218
219         /* copy the file */
220         if (bb_copyfd_eof(fd_data, fd_local) == -1) {
221                 /* error msg is already printed by bb_copyfd_eof */
222                 return EXIT_FAILURE;
223         }
224
225         /* close it all down */
226         close(fd_data);
227         if (ftpcmd(NULL, NULL, control_stream) != 226) {
228                 ftp_die(NULL);
229         }
230         ftpcmd("QUIT", NULL, control_stream);
231
232         return EXIT_SUCCESS;
233 }
234 #endif
235
236 #if !ENABLE_FTPPUT
237 int ftp_send(FILE *control_stream,
238                 const char *server_path, char *local_path);
239 #else
240 static
241 int ftp_send(FILE *control_stream,
242                 const char *server_path, char *local_path)
243 {
244         int fd_data;
245         int fd_local;
246         int response;
247
248         /* connect to the data socket */
249         if (ftpcmd("PASV", NULL, control_stream) != 227) {
250                 ftp_die("PASV");
251         }
252         fd_data = xconnect_ftpdata();
253
254         /* get the local file */
255         fd_local = STDIN_FILENO;
256         if (NOT_LONE_DASH(local_path))
257                 fd_local = xopen(local_path, O_RDONLY);
258
259         response = ftpcmd("STOR", server_path, control_stream);
260         switch (response) {
261         case 125:
262         case 150:
263                 break;
264         default:
265                 ftp_die("STOR");
266         }
267
268         /* transfer the file  */
269         if (bb_copyfd_eof(fd_local, fd_data) == -1) {
270                 /* error msg is already printed by bb_copyfd_eof */
271                 return EXIT_FAILURE;
272         }
273
274         /* close it all down */
275         close(fd_data);
276         if (ftpcmd(NULL, NULL, control_stream) != 226) {
277                 ftp_die("close");
278         }
279         ftpcmd("QUIT", NULL, control_stream);
280
281         return EXIT_SUCCESS;
282 }
283 #endif
284
285 #define FTPGETPUT_OPT_CONTINUE  1
286 #define FTPGETPUT_OPT_VERBOSE   2
287 #define FTPGETPUT_OPT_USER      4
288 #define FTPGETPUT_OPT_PASSWORD  8
289 #define FTPGETPUT_OPT_PORT      16
290
291 #if ENABLE_FEATURE_FTPGETPUT_LONG_OPTIONS
292 static const char ftpgetput_longopts[] ALIGN1 =
293         "continue\0" Required_argument "c"
294         "verbose\0"  No_argument       "v"
295         "username\0" Required_argument "u"
296         "password\0" Required_argument "p"
297         "port\0"     Required_argument "P"
298         ;
299 #endif
300
301 int ftpgetput_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
302 int ftpgetput_main(int argc ATTRIBUTE_UNUSED, char **argv)
303 {
304         unsigned opt;
305         const char *port = "ftp";
306         /* socket to ftp server */
307         FILE *control_stream;
308
309 #if ENABLE_FTPPUT && !ENABLE_FTPGET
310 # define ftp_action ftp_send
311 #elif ENABLE_FTPGET && !ENABLE_FTPPUT
312 # define ftp_action ftp_receive
313 #else
314         int (*ftp_action)(FILE *, const char *, char *) = ftp_send;
315
316         /* Check to see if the command is ftpget or ftput */
317         if (applet_name[3] == 'g') {
318                 ftp_action = ftp_receive;
319         }
320 #endif
321
322         INIT_G();
323         /* Set default values */
324         user = "anonymous";
325         password = "busybox@";
326
327         /*
328          * Decipher the command line
329          */
330 #if ENABLE_FEATURE_FTPGETPUT_LONG_OPTIONS
331         applet_long_options = ftpgetput_longopts;
332 #endif
333         opt_complementary = "=3:vv:cc"; /* must have 3 params; -v and -c count */
334         opt = getopt32(argv, "cvu:p:P:", &user, &password, &port,
335                                         &verbose_flag, &do_continue);
336         argv += optind;
337
338         /* We want to do exactly _one_ DNS lookup, since some
339          * sites (i.e. ftp.us.debian.org) use round-robin DNS
340          * and we want to connect to only one IP... */
341         lsa = xhost2sockaddr(argv[0], bb_lookup_port(port, "tcp", 21));
342         if (verbose_flag) {
343                 printf("Connecting to %s (%s)\n", argv[0],
344                         xmalloc_sockaddr2dotted(&lsa->u.sa));
345         }
346
347         /*  Connect/Setup/Configure the FTP session */
348         control_stream = ftp_login();
349
350         return ftp_action(control_stream, argv[1], argv[2]);
351 }