ftp.c revision 60707
1/*-
2 * Copyright (c) 1998 Dag-Erling Co�dan Sm�rgrav
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 * 1. Redistributions of source code must retain the above copyright
9 *    notice, this list of conditions and the following disclaimer
10 *    in this position and unchanged.
11 * 2. Redistributions in binary form must reproduce the above copyright
12 *    notice, this list of conditions and the following disclaimer in the
13 *    documentation and/or other materials provided with the distribution.
14 * 3. The name of the author may not be used to endorse or promote products
15 *    derived from this software without specific prior written permission
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
18 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
19 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
21 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
22 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 *
28 * $FreeBSD: head/lib/libfetch/ftp.c 60707 2000-05-19 09:45:42Z des $
29 */
30
31/*
32 * Portions of this code were taken from or based on ftpio.c:
33 *
34 * ----------------------------------------------------------------------------
35 * "THE BEER-WARE LICENSE" (Revision 42):
36 * <phk@login.dknet.dk> wrote this file.  As long as you retain this notice you
37 * can do whatever you want with this stuff. If we meet some day, and you think
38 * this stuff is worth it, you can buy me a beer in return.   Poul-Henning Kamp
39 * ----------------------------------------------------------------------------
40 *
41 * Major Changelog:
42 *
43 * Dag-Erling Co�dan Sm�rgrav
44 * 9 Jun 1998
45 *
46 * Incorporated into libfetch
47 *
48 * Jordan K. Hubbard
49 * 17 Jan 1996
50 *
51 * Turned inside out. Now returns xfers as new file ids, not as a special
52 * `state' of FTP_t
53 *
54 * $ftpioId: ftpio.c,v 1.30 1998/04/11 07:28:53 phk Exp $
55 *
56 */
57
58#include <sys/param.h>
59#include <sys/socket.h>
60#include <sys/uio.h>
61#include <netinet/in.h>
62
63#include <ctype.h>
64#include <errno.h>
65#include <netdb.h>
66#include <stdarg.h>
67#include <stdio.h>
68#include <stdlib.h>
69#include <string.h>
70#include <time.h>
71#include <unistd.h>
72
73#include "fetch.h"
74#include "common.h"
75#include "ftperr.h"
76
77#define FTP_ANONYMOUS_USER	"ftp"
78#define FTP_ANONYMOUS_PASSWORD	"ftp"
79#define FTP_DEFAULT_PORT 21
80
81#define FTP_OPEN_DATA_CONNECTION	150
82#define FTP_OK				200
83#define FTP_FILE_STATUS			213
84#define FTP_SERVICE_READY		220
85#define FTP_PASSIVE_MODE		227
86#define FTP_LOGGED_IN			230
87#define FTP_FILE_ACTION_OK		250
88#define FTP_NEED_PASSWORD		331
89#define FTP_NEED_ACCOUNT		332
90#define FTP_FILE_OK			350
91#define FTP_SYNTAX_ERROR		500
92
93static char ENDL[2] = "\r\n";
94
95static struct url cached_host;
96static int cached_socket;
97
98static char *last_reply;
99static size_t lr_size, lr_length;
100static int last_code;
101
102#define isftpreply(foo) (isdigit(foo[0]) && isdigit(foo[1]) \
103			 && isdigit(foo[2]) \
104                         && (foo[3] == ' ' || foo[3] == '\0'))
105#define isftpinfo(foo) (isdigit(foo[0]) && isdigit(foo[1]) \
106			&& isdigit(foo[2]) && foo[3] == '-')
107
108/*
109 * Get server response
110 */
111static int
112_ftp_chkerr(int cd)
113{
114    do {
115	if (_fetch_getln(cd, &last_reply, &lr_size, &lr_length) == -1) {
116	    _fetch_syserr();
117	    return -1;
118	}
119#ifndef NDEBUG
120	_fetch_info("got reply '%.*s'", lr_length - 2, last_reply);
121#endif
122    } while (isftpinfo(last_reply));
123
124    while (lr_length && isspace(last_reply[lr_length-1]))
125	lr_length--;
126    last_reply[lr_length] = 0;
127
128    if (!isftpreply(last_reply)) {
129	_ftp_seterr(999);
130	return -1;
131    }
132
133    last_code = (last_reply[0] - '0') * 100
134	+ (last_reply[1] - '0') * 10
135	+ (last_reply[2] - '0');
136
137    return last_code;
138}
139
140/*
141 * Send a command and check reply
142 */
143static int
144_ftp_cmd(int cd, char *fmt, ...)
145{
146    va_list ap;
147    struct iovec iov[2];
148    char *msg;
149    int r;
150
151    va_start(ap, fmt);
152    vasprintf(&msg, fmt, ap);
153    va_end(ap);
154
155    if (msg == NULL) {
156	errno = ENOMEM;
157	_fetch_syserr();
158	return -1;
159    }
160#ifndef NDEBUG
161    _fetch_info("sending '%s'", msg);
162#endif
163    iov[0].iov_base = msg;
164    iov[0].iov_len = strlen(msg);
165    iov[1].iov_base = ENDL;
166    iov[1].iov_len = sizeof ENDL;
167    r = writev(cd, iov, 2);
168    free(msg);
169    if (r == -1) {
170	_fetch_syserr();
171	return -1;
172    }
173
174    return _ftp_chkerr(cd);
175}
176
177/*
178 * Transfer file
179 */
180static FILE *
181_ftp_transfer(int cd, char *oper, char *file,
182	      char *mode, off_t offset, char *flags)
183{
184    struct sockaddr_in sin;
185    int pasv, high, verbose;
186    int e, sd = -1;
187    socklen_t l;
188    char *s;
189    FILE *df;
190
191    /* check flags */
192    pasv = (flags && strchr(flags, 'p'));
193    high = (flags && strchr(flags, 'h'));
194    verbose = (flags && strchr(flags, 'v'));
195
196    /* change directory */
197    if (((s = strrchr(file, '/')) != NULL) && (s != file)) {
198	*s = 0;
199	if (verbose)
200	    _fetch_info("changing directory to %s", file);
201	if ((e = _ftp_cmd(cd, "CWD %s", file)) != FTP_FILE_ACTION_OK) {
202	    *s = '/';
203	    if (e != -1)
204		_ftp_seterr(e);
205	    return NULL;
206	}
207	*s++ = '/';
208    } else {
209	if (verbose)
210	    _fetch_info("changing directory to /");
211	if ((e = _ftp_cmd(cd, "CWD /")) != FTP_FILE_ACTION_OK) {
212	    if (e != -1)
213		_ftp_seterr(e);
214	    return NULL;
215	}
216    }
217
218    /* s now points to file name */
219
220    /* open data socket */
221    if ((sd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) {
222	_fetch_syserr();
223	return NULL;
224    }
225
226    if (pasv) {
227	u_char addr[6];
228	char *ln, *p;
229	int i;
230
231	/* send PASV command */
232	if (verbose)
233	    _fetch_info("setting passive mode");
234	if ((e = _ftp_cmd(cd, "PASV")) != FTP_PASSIVE_MODE)
235	    goto ouch;
236
237	/*
238	 * Find address and port number. The reply to the PASV command
239         * is IMHO the one and only weak point in the FTP protocol.
240	 */
241	ln = last_reply;
242	for (p = ln + 3; *p && !isdigit(*p); p++)
243	    /* nothing */ ;
244	for (i = 0; *p, i < 6; i++, p++)
245	    addr[i] = strtol(p, &p, 10);
246	if (i < 6) {
247	    e = 999;
248	    goto ouch;
249	}
250
251	/* seek to required offset */
252	if (offset)
253	    if (_ftp_cmd(cd, "REST %lu", (u_long)offset) != FTP_FILE_OK)
254		goto sysouch;
255
256	/* construct sockaddr for data socket */
257	l = sizeof sin;
258	if (getpeername(cd, (struct sockaddr *)&sin, &l) == -1)
259	    goto sysouch;
260	bcopy(addr, (char *)&sin.sin_addr, 4);
261	bcopy(addr + 4, (char *)&sin.sin_port, 2);
262
263	/* connect to data port */
264	if (verbose)
265	    _fetch_info("opening data connection");
266	if (connect(sd, (struct sockaddr *)&sin, sizeof sin) == -1)
267	    goto sysouch;
268
269	/* make the server initiate the transfer */
270	if (verbose)
271	    _fetch_info("initiating transfer");
272	e = _ftp_cmd(cd, "%s %s", oper, s);
273	if (e != FTP_OPEN_DATA_CONNECTION)
274	    goto ouch;
275
276    } else {
277	u_int32_t a;
278	u_short p;
279	int arg, d;
280
281	/* find our own address, bind, and listen */
282	l = sizeof sin;
283	if (getsockname(cd, (struct sockaddr *)&sin, &l) == -1)
284	    goto sysouch;
285	sin.sin_port = 0;
286	arg = high ? IP_PORTRANGE_HIGH : IP_PORTRANGE_DEFAULT;
287	if (setsockopt(sd, IPPROTO_IP, IP_PORTRANGE,
288		       (char *)&arg, sizeof arg) == -1)
289	    goto sysouch;
290	if (verbose)
291	    _fetch_info("binding data socket");
292	if (bind(sd, (struct sockaddr *)&sin, l) == -1)
293	    goto sysouch;
294	if (listen(sd, 1) == -1)
295	    goto sysouch;
296
297	/* find what port we're on and tell the server */
298	if (getsockname(sd, (struct sockaddr *)&sin, &l) == -1)
299	    goto sysouch;
300	a = ntohl(sin.sin_addr.s_addr);
301	p = ntohs(sin.sin_port);
302	e = _ftp_cmd(cd, "PORT %d,%d,%d,%d,%d,%d",
303		     (a >> 24) & 0xff, (a >> 16) & 0xff,
304		     (a >> 8) & 0xff, a & 0xff,
305		     (p >> 8) & 0xff, p & 0xff);
306	if (e != FTP_OK)
307	    goto ouch;
308
309	/* make the server initiate the transfer */
310	if (verbose)
311	    _fetch_info("initiating transfer");
312	e = _ftp_cmd(cd, "%s %s", oper, s);
313	if (e != FTP_OPEN_DATA_CONNECTION)
314	    goto ouch;
315
316	/* accept the incoming connection and go to town */
317	if ((d = accept(sd, NULL, NULL)) == -1)
318	    goto sysouch;
319	close(sd);
320	sd = d;
321    }
322
323    if ((df = fdopen(sd, mode)) == NULL)
324	goto sysouch;
325    return df;
326
327sysouch:
328    _fetch_syserr();
329    close(sd);
330    return NULL;
331
332ouch:
333    if (e != -1)
334	_ftp_seterr(e);
335    close(sd);
336    return NULL;
337}
338
339/*
340 * Log on to FTP server
341 */
342static int
343_ftp_connect(char *host, int port, char *user, char *pwd, char *flags)
344{
345    int cd, e, pp = 0, direct, verbose;
346    char *p, *q;
347
348    direct = (flags && strchr(flags, 'd'));
349    verbose = (flags && strchr(flags, 'v'));
350
351    /* check for proxy */
352    if (!direct && (p = getenv("FTP_PROXY")) != NULL) {
353	if ((q = strchr(p, ':')) != NULL) {
354	    if (strspn(q+1, "0123456789") != strlen(q+1) || strlen(q+1) > 5) {
355		/* XXX we should emit some kind of warning */
356	    }
357	    pp = atoi(q+1);
358	    if (pp < 1 || pp > 65535) {
359		/* XXX we should emit some kind of warning */
360	    }
361	}
362	if (!pp) {
363	    struct servent *se;
364
365	    if ((se = getservbyname("ftp", "tcp")) != NULL)
366		pp = ntohs(se->s_port);
367	    else
368		pp = FTP_DEFAULT_PORT;
369	}
370	if (q)
371	    *q = 0;
372	cd = _fetch_connect(p, pp, verbose);
373	if (q)
374	    *q = ':';
375    } else {
376	/* no proxy, go straight to target */
377	cd = _fetch_connect(host, port, verbose);
378	p = NULL;
379    }
380
381    /* check connection */
382    if (cd == -1) {
383	_fetch_syserr();
384	return NULL;
385    }
386
387    /* expect welcome message */
388    if ((e = _ftp_chkerr(cd)) != FTP_SERVICE_READY)
389	goto fouch;
390
391    /* send user name and password */
392    if (!user || !*user)
393	user = FTP_ANONYMOUS_USER;
394    e = p ? _ftp_cmd(cd, "USER %s@%s@%d", user, host, port)
395	  : _ftp_cmd(cd, "USER %s", user);
396
397    /* did the server request a password? */
398    if (e == FTP_NEED_PASSWORD) {
399	if (!pwd || !*pwd)
400	    pwd = FTP_ANONYMOUS_PASSWORD;
401	e = _ftp_cmd(cd, "PASS %s", pwd);
402    }
403
404    /* did the server request an account? */
405    if (e == FTP_NEED_ACCOUNT)
406	goto fouch;
407
408    /* we should be done by now */
409    if (e != FTP_LOGGED_IN)
410	goto fouch;
411
412    /* might as well select mode and type at once */
413#ifdef FTP_FORCE_STREAM_MODE
414    if ((e = _ftp_cmd(cd, "MODE S")) != FTP_OK) /* default is S */
415	goto fouch;
416#endif
417    if ((e = _ftp_cmd(cd, "TYPE I")) != FTP_OK) /* default is A */
418	goto fouch;
419
420    /* done */
421    return cd;
422
423fouch:
424    if (e != -1)
425	_ftp_seterr(e);
426    close(cd);
427    return NULL;
428}
429
430/*
431 * Disconnect from server
432 */
433static void
434_ftp_disconnect(int cd)
435{
436    (void)_ftp_cmd(cd, "QUIT");
437    close(cd);
438}
439
440/*
441 * Check if we're already connected
442 */
443static int
444_ftp_isconnected(struct url *url)
445{
446    return (cached_socket
447	    && (strcmp(url->host, cached_host.host) == 0)
448	    && (strcmp(url->user, cached_host.user) == 0)
449	    && (strcmp(url->pwd, cached_host.pwd) == 0)
450	    && (url->port == cached_host.port));
451}
452
453/*
454 * Check the cache, reconnect if no luck
455 */
456static int
457_ftp_cached_connect(struct url *url, char *flags)
458{
459    int e, cd;
460
461    cd = -1;
462
463    /* set default port */
464    if (!url->port) {
465	struct servent *se;
466
467	if ((se = getservbyname("ftp", "tcp")) != NULL)
468	    url->port = ntohs(se->s_port);
469	else
470	    url->port = FTP_DEFAULT_PORT;
471    }
472
473    /* try to use previously cached connection */
474    if (_ftp_isconnected(url)) {
475	e = _ftp_cmd(cached_socket, "NOOP");
476	if (e == FTP_OK || e == FTP_SYNTAX_ERROR)
477	    cd = cached_socket;
478    }
479
480    /* connect to server */
481    if (cd == -1) {
482	cd = _ftp_connect(url->host, url->port, url->user, url->pwd, flags);
483	if (cd == -1)
484	    return -1;
485	if (cached_socket)
486	    _ftp_disconnect(cached_socket);
487	cached_socket = cd;
488	memcpy(&cached_host, url, sizeof *url);
489    }
490
491    return cd;
492}
493
494/*
495 * Get file
496 */
497FILE *
498fetchGetFTP(struct url *url, char *flags)
499{
500    int cd;
501
502    /* connect to server */
503    if ((cd = _ftp_cached_connect(url, flags)) == NULL)
504	return NULL;
505
506    /* initiate the transfer */
507    return _ftp_transfer(cd, "RETR", url->doc, "r", url->offset, flags);
508}
509
510/*
511 * Put file
512 */
513FILE *
514fetchPutFTP(struct url *url, char *flags)
515{
516    int cd;
517
518    /* connect to server */
519    if ((cd = _ftp_cached_connect(url, flags)) == NULL)
520	return NULL;
521
522    /* initiate the transfer */
523    return _ftp_transfer(cd, (flags && strchr(flags, 'a')) ? "APPE" : "STOR",
524			 url->doc, "w", url->offset, flags);
525}
526
527/*
528 * Get file stats
529 */
530int
531fetchStatFTP(struct url *url, struct url_stat *us, char *flags)
532{
533    char *ln, *s;
534    struct tm tm;
535    time_t t;
536    int e, cd;
537
538    us->size = -1;
539    us->atime = us->mtime = 0;
540
541    /* connect to server */
542    if ((cd = _ftp_cached_connect(url, flags)) == NULL)
543	return -1;
544
545    /* change directory */
546    if (((s = strrchr(url->doc, '/')) != NULL) && (s != url->doc)) {
547	*s = 0;
548	if ((e = _ftp_cmd(cd, "CWD %s", url->doc)) != FTP_FILE_ACTION_OK) {
549	    *s = '/';
550	    goto ouch;
551	}
552	*s++ = '/';
553    } else {
554	if ((e = _ftp_cmd(cd, "CWD /")) != FTP_FILE_ACTION_OK)
555	    goto ouch;
556    }
557
558    /* s now points to file name */
559
560    if (_ftp_cmd(cd, "SIZE %s", s) != FTP_FILE_STATUS)
561	goto ouch;
562    for (ln = last_reply + 4; *ln && isspace(*ln); ln++)
563	/* nothing */ ;
564    for (us->size = 0; *ln && isdigit(*ln); ln++)
565	us->size = us->size * 10 + *ln - '0';
566    if (*ln && !isspace(*ln)) {
567	_ftp_seterr(999);
568	return -1;
569    }
570    DEBUG(fprintf(stderr, "size: [\033[1m%lld\033[m]\n", us->size));
571
572    if ((e = _ftp_cmd(cd, "MDTM %s", s)) != FTP_FILE_STATUS)
573	goto ouch;
574    for (ln = last_reply + 4; *ln && isspace(*ln); ln++)
575	/* nothing */ ;
576    e = 999;
577    switch (strspn(ln, "0123456789")) {
578    case 14:
579	break;
580    case 15:
581	ln++;
582	ln[0] = '2';
583	ln[1] = '0';
584	break;
585    default:
586	goto ouch;
587    }
588    if (sscanf(ln, "%04d%02d%02d%02d%02d%02d",
589	       &tm.tm_year, &tm.tm_mon, &tm.tm_mday,
590	       &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 6)
591	goto ouch;
592    tm.tm_mon--;
593    tm.tm_year -= 1900;
594    tm.tm_isdst = -1;
595    t = timegm(&tm);
596    if (t == (time_t)-1)
597	t = time(NULL);
598    us->mtime = t;
599    us->atime = t;
600    DEBUG(fprintf(stderr, "last modified: [\033[1m%04d-%02d-%02d "
601		  "%02d:%02d:%02d\033[m]\n",
602		  tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
603		  tm.tm_hour, tm.tm_min, tm.tm_sec));
604    return 0;
605
606ouch:
607    if (e != -1)
608	_ftp_seterr(e);
609    return -1;
610}
611
612/*
613 * List a directory
614 */
615extern void warnx(char *, ...);
616struct url_ent *
617fetchListFTP(struct url *url, char *flags)
618{
619    warnx("fetchListFTP(): not implemented");
620    return NULL;
621}
622