ftp.c revision 56635
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 56635 2000-01-26 12:50:46Z 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 <stdarg.h>
66#include <stdio.h>
67#include <stdlib.h>
68#include <string.h>
69#include <time.h>
70#include <unistd.h>
71
72#include "fetch.h"
73#include "common.h"
74#include "ftperr.h"
75
76#define FTP_ANONYMOUS_USER	"ftp"
77#define FTP_ANONYMOUS_PASSWORD	"ftp"
78#define FTP_DEFAULT_PORT 21
79
80#define FTP_OPEN_DATA_CONNECTION	150
81#define FTP_OK				200
82#define FTP_FILE_STATUS			213
83#define FTP_SERVICE_READY		220
84#define FTP_PASSIVE_MODE		227
85#define FTP_LOGGED_IN			230
86#define FTP_FILE_ACTION_OK		250
87#define FTP_NEED_PASSWORD		331
88#define FTP_NEED_ACCOUNT		332
89#define FTP_SYNTAX_ERROR		500
90
91static char ENDL[2] = "\r\n";
92
93static struct url cached_host;
94static int cached_socket;
95
96static char *last_reply;
97static size_t lr_size, lr_length;
98static int last_code;
99
100#define isftpreply(foo) (isdigit(foo[0]) && isdigit(foo[1]) \
101			 && isdigit(foo[2]) && foo[3] == ' ')
102#define isftpinfo(foo) (isdigit(foo[0]) && isdigit(foo[1]) \
103			&& isdigit(foo[2]) && foo[3] == '-')
104
105/*
106 * Get server response
107 */
108static int
109_ftp_chkerr(int cd)
110{
111    do {
112	if (_fetch_getln(cd, &last_reply, &lr_size, &lr_length) == -1) {
113	    _fetch_syserr();
114	    return -1;
115	}
116#ifndef NDEBUG
117	_fetch_info("got reply '%.*s'", lr_length - 2, last_reply);
118#endif
119    } while (isftpinfo(last_reply));
120
121    while (lr_length && isspace(last_reply[lr_length-1]))
122	lr_length--;
123    last_reply[lr_length] = 0;
124
125    if (!isftpreply(last_reply)) {
126	_ftp_seterr(999);
127	return -1;
128    }
129
130    last_code = (last_reply[0] - '0') * 100
131	+ (last_reply[1] - '0') * 10
132	+ (last_reply[2] - '0');
133
134    return last_code;
135}
136
137/*
138 * Send a command and check reply
139 */
140static int
141_ftp_cmd(int cd, char *fmt, ...)
142{
143    va_list ap;
144    struct iovec iov[2];
145    char *msg;
146    int r;
147
148    va_start(ap, fmt);
149    vasprintf(&msg, fmt, ap);
150    va_end(ap);
151
152    if (msg == NULL) {
153	errno = ENOMEM;
154	_fetch_syserr();
155	return -1;
156    }
157#ifndef NDEBUG
158    _fetch_info("sending '%s'", msg);
159#endif
160    iov[0].iov_base = msg;
161    iov[0].iov_len = strlen(msg);
162    iov[1].iov_base = ENDL;
163    iov[1].iov_len = sizeof(ENDL);
164    r = writev(cd, iov, 2);
165    free(msg);
166    if (r == -1) {
167	_fetch_syserr();
168	return -1;
169    }
170
171    return _ftp_chkerr(cd);
172}
173
174/*
175 * Transfer file
176 */
177static FILE *
178_ftp_transfer(int cd, char *oper, char *file, char *mode, char *flags)
179{
180    struct sockaddr_in sin;
181    int pasv, high, verbose;
182    int e, sd = -1;
183    socklen_t l;
184    char *s;
185    FILE *df;
186
187    /* check flags */
188    pasv = (flags && strchr(flags, 'p'));
189    high = (flags && strchr(flags, 'h'));
190    verbose = (flags && strchr(flags, 'v'));
191
192    /* change directory */
193    if (((s = strrchr(file, '/')) != NULL) && (s != file)) {
194	*s = 0;
195	if (verbose)
196	    _fetch_info("changing directory to %s", file);
197	if ((e = _ftp_cmd(cd, "CWD %s", file)) != FTP_FILE_ACTION_OK) {
198	    *s = '/';
199	    if (e != -1)
200		_ftp_seterr(e);
201	    return NULL;
202	}
203	*s++ = '/';
204    } else {
205	if (verbose)
206	    _fetch_info("changing directory to /");
207	if ((e = _ftp_cmd(cd, "CWD /")) != FTP_FILE_ACTION_OK) {
208	    if (e != -1)
209		_ftp_seterr(e);
210	    return NULL;
211	}
212    }
213
214    /* s now points to file name */
215
216    /* open data socket */
217    if ((sd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) {
218	_fetch_syserr();
219	return NULL;
220    }
221
222    if (pasv) {
223	u_char addr[6];
224	char *ln, *p;
225	int i;
226
227	/* send PASV command */
228	if (verbose)
229	    _fetch_info("setting passive mode");
230	if ((e = _ftp_cmd(cd, "PASV")) != FTP_PASSIVE_MODE)
231	    goto ouch;
232
233	/*
234	 * Find address and port number. The reply to the PASV command
235         * is IMHO the one and only weak point in the FTP protocol.
236	 */
237	ln = last_reply;
238	for (p = ln + 3; !isdigit(*p); p++)
239	    /* nothing */ ;
240	for (p--, i = 0; i < 6; i++) {
241	    p++; /* skip the comma */
242	    addr[i] = strtol(p, &p, 10);
243	}
244
245	/* construct sockaddr for data socket */
246	l = sizeof(sin);
247	if (getpeername(cd, (struct sockaddr *)&sin, &l) == -1)
248	    goto sysouch;
249	bcopy(addr, (char *)&sin.sin_addr, 4);
250	bcopy(addr + 4, (char *)&sin.sin_port, 2);
251
252	/* connect to data port */
253	if (verbose)
254	    _fetch_info("opening data connection");
255	if (connect(sd, (struct sockaddr *)&sin, sizeof(sin)) == -1)
256	    goto sysouch;
257
258	/* make the server initiate the transfer */
259	if (verbose)
260	    _fetch_info("initiating transfer");
261	e = _ftp_cmd(cd, "%s %s", oper, s);
262	if (e != FTP_OPEN_DATA_CONNECTION)
263	    goto ouch;
264
265    } else {
266	u_int32_t a;
267	u_short p;
268	int arg, d;
269
270	/* find our own address, bind, and listen */
271	l = sizeof(sin);
272	if (getsockname(cd, (struct sockaddr *)&sin, &l) == -1)
273	    goto sysouch;
274	sin.sin_port = 0;
275	arg = high ? IP_PORTRANGE_HIGH : IP_PORTRANGE_DEFAULT;
276	if (setsockopt(sd, IPPROTO_IP, IP_PORTRANGE,
277		       (char *)&arg, sizeof(arg)) == -1)
278	    goto sysouch;
279	if (verbose)
280	    _fetch_info("binding data socket");
281	if (bind(sd, (struct sockaddr *)&sin, l) == -1)
282	    goto sysouch;
283	if (listen(sd, 1) == -1)
284	    goto sysouch;
285
286	/* find what port we're on and tell the server */
287	if (getsockname(sd, (struct sockaddr *)&sin, &l) == -1)
288	    goto sysouch;
289	a = ntohl(sin.sin_addr.s_addr);
290	p = ntohs(sin.sin_port);
291	e = _ftp_cmd(cd, "PORT %d,%d,%d,%d,%d,%d",
292		     (a >> 24) & 0xff, (a >> 16) & 0xff,
293		     (a >> 8) & 0xff, a & 0xff,
294		     (p >> 8) & 0xff, p & 0xff);
295	if (e != FTP_OK)
296	    goto ouch;
297
298	/* make the server initiate the transfer */
299	if (verbose)
300	    _fetch_info("initiating transfer");
301	e = _ftp_cmd(cd, "%s %s", oper, s);
302	if (e != FTP_OPEN_DATA_CONNECTION)
303	    goto ouch;
304
305	/* accept the incoming connection and go to town */
306	if ((d = accept(sd, NULL, NULL)) == -1)
307	    goto sysouch;
308	close(sd);
309	sd = d;
310    }
311
312    if ((df = fdopen(sd, mode)) == NULL)
313	goto sysouch;
314    return df;
315
316sysouch:
317    _fetch_syserr();
318    close(sd);
319    return NULL;
320
321ouch:
322    if (e != -1)
323	_ftp_seterr(e);
324    close(sd);
325    return NULL;
326}
327
328/*
329 * Log on to FTP server
330 */
331static int
332_ftp_connect(char *host, int port, char *user, char *pwd, char *flags)
333{
334    int cd, e, pp = FTP_DEFAULT_PORT, direct, verbose;
335    char *p, *q;
336
337    direct = (flags && strchr(flags, 'd'));
338    verbose = (flags && strchr(flags, 'v'));
339
340    /* check for proxy */
341    if (!direct && (p = getenv("FTP_PROXY")) != NULL) {
342	if ((q = strchr(p, ':')) != NULL) {
343	    /* XXX check that it's a valid number */
344	    pp = atoi(q+1);
345	}
346	if (q)
347	    *q = 0;
348	cd = _fetch_connect(p, pp, verbose);
349	if (q)
350	    *q = ':';
351    } else {
352	/* no proxy, go straight to target */
353	cd = _fetch_connect(host, port, verbose);
354	p = NULL;
355    }
356
357    /* check connection */
358    if (cd == -1) {
359	_fetch_syserr();
360	return NULL;
361    }
362
363    /* expect welcome message */
364    if ((e = _ftp_chkerr(cd)) != FTP_SERVICE_READY)
365	goto fouch;
366
367    /* send user name and password */
368    if (!user || !*user)
369	user = FTP_ANONYMOUS_USER;
370    e = p ? _ftp_cmd(cd, "USER %s@%s@%d", user, host, port)
371	  : _ftp_cmd(cd, "USER %s", user);
372
373    /* did the server request a password? */
374    if (e == FTP_NEED_PASSWORD) {
375	if (!pwd || !*pwd)
376	    pwd = FTP_ANONYMOUS_PASSWORD;
377	e = _ftp_cmd(cd, "PASS %s", pwd);
378    }
379
380    /* did the server request an account? */
381    if (e == FTP_NEED_ACCOUNT)
382	goto fouch;
383
384    /* we should be done by now */
385    if (e != FTP_LOGGED_IN)
386	goto fouch;
387
388    /* might as well select mode and type at once */
389#ifdef FTP_FORCE_STREAM_MODE
390    if ((e = _ftp_cmd(cd, "MODE S")) != FTP_OK) /* default is S */
391	goto fouch;
392#endif
393    if ((e = _ftp_cmd(cd, "TYPE I")) != FTP_OK) /* default is A */
394	goto fouch;
395
396    /* done */
397    return cd;
398
399fouch:
400    if (e != -1)
401	_ftp_seterr(e);
402    close(cd);
403    return NULL;
404}
405
406/*
407 * Disconnect from server
408 */
409static void
410_ftp_disconnect(int cd)
411{
412    (void)_ftp_cmd(cd, "QUIT");
413    close(cd);
414}
415
416/*
417 * Check if we're already connected
418 */
419static int
420_ftp_isconnected(struct url *url)
421{
422    return (cached_socket
423	    && (strcmp(url->host, cached_host.host) == 0)
424	    && (strcmp(url->user, cached_host.user) == 0)
425	    && (strcmp(url->pwd, cached_host.pwd) == 0)
426	    && (url->port == cached_host.port));
427}
428
429/*
430 * Check the cache, reconnect if no luck
431 */
432static int
433_ftp_cached_connect(struct url *url, char *flags)
434{
435    int e, cd;
436
437    cd = -1;
438
439    /* set default port */
440    if (!url->port)
441	url->port = FTP_DEFAULT_PORT;
442
443    /* try to use previously cached connection */
444    if (_ftp_isconnected(url)) {
445	e = _ftp_cmd(cached_socket, "NOOP");
446	if (e == FTP_OK || e == FTP_SYNTAX_ERROR)
447	    cd = cached_socket;
448    }
449
450    /* connect to server */
451    if (cd == -1) {
452	cd = _ftp_connect(url->host, url->port, url->user, url->pwd, flags);
453	if (cd == -1)
454	    return -1;
455	if (cached_socket)
456	    _ftp_disconnect(cached_socket);
457	cached_socket = cd;
458	memcpy(&cached_host, url, sizeof(struct url));
459    }
460
461    return cd;
462}
463
464/*
465 * Get file
466 */
467FILE *
468fetchGetFTP(struct url *url, char *flags)
469{
470    int cd;
471
472    /* connect to server */
473    if ((cd = _ftp_cached_connect(url, flags)) == NULL)
474	return NULL;
475
476    /* initiate the transfer */
477    return _ftp_transfer(cd, "RETR", url->doc, "r", flags);
478}
479
480/*
481 * Put file
482 */
483FILE *
484fetchPutFTP(struct url *url, char *flags)
485{
486    int cd;
487
488    /* connect to server */
489    if ((cd = _ftp_cached_connect(url, flags)) == NULL)
490	return NULL;
491
492    /* initiate the transfer */
493    return _ftp_transfer(cd, (flags && strchr(flags, 'a')) ? "APPE" : "STOR",
494			 url->doc, "w", flags);
495}
496
497/*
498 * Get file stats
499 */
500int
501fetchStatFTP(struct url *url, struct url_stat *us, char *flags)
502{
503    char *ln, *s;
504    struct tm tm;
505    time_t t;
506    int e, cd;
507
508    /* connect to server */
509    if ((cd = _ftp_cached_connect(url, flags)) == NULL)
510	return -1;
511
512    /* change directory */
513    if (((s = strrchr(url->doc, '/')) != NULL) && (s != url->doc)) {
514	*s = 0;
515	if ((e = _ftp_cmd(cd, "CWD %s", url->doc)) != FTP_FILE_ACTION_OK) {
516	    *s = '/';
517	    goto ouch;
518	}
519	*s++ = '/';
520    } else {
521	if ((e = _ftp_cmd(cd, "CWD /")) != FTP_FILE_ACTION_OK)
522	    goto ouch;
523    }
524
525    /* s now points to file name */
526
527    if (_ftp_cmd(cd, "SIZE %s", s) != FTP_FILE_STATUS)
528	goto ouch;
529    for (ln = last_reply + 4; *ln && isspace(*ln); ln++)
530	/* nothing */ ;
531    for (us->size = 0; *ln && isdigit(*ln); ln++)
532	us->size = us->size * 10 + *ln - '0';
533    if (*ln && !isspace(*ln)) {
534	_ftp_seterr(999);
535	return -1;
536    }
537
538    if ((e = _ftp_cmd(cd, "MDTM %s", s)) != FTP_FILE_STATUS)
539	goto ouch;
540    for (ln = last_reply + 4; *ln && isspace(*ln); ln++)
541	/* nothing */ ;
542    sscanf(ln, "%04d%02d%02d%02d%02d%02d",
543	   &tm.tm_year, &tm.tm_mon, &tm.tm_mday,
544	   &tm.tm_hour, &tm.tm_min, &tm.tm_sec);
545    /* XXX should check the return value from sscanf */
546    tm.tm_mon--;
547    tm.tm_year -= 1900;
548    tm.tm_isdst = -1;
549    t = mktime(&tm);
550    if (t == (time_t)-1)
551	t = time(NULL);
552    else
553	t += tm.tm_gmtoff;
554    us->mtime = t;
555    us->atime = t;
556    return 0;
557
558ouch:
559    if (e != -1)
560	_ftp_seterr(e);
561    return -1;
562}
563
564/*
565 * List a directory
566 */
567extern void warnx(char *, ...);
568struct url_ent *
569fetchListFTP(struct url *url, char *flags)
570{
571    warnx("fetchListFTP(): not implemented");
572    return NULL;
573}
574