ftp.c revision 41989
137535Sdes/*-
237535Sdes * Copyright (c) 1998 Dag-Erling Co�dan Sm�rgrav
337535Sdes * All rights reserved.
437535Sdes *
537535Sdes * Redistribution and use in source and binary forms, with or without
637535Sdes * modification, are permitted provided that the following conditions
737535Sdes * are met:
837535Sdes * 1. Redistributions of source code must retain the above copyright
937535Sdes *    notice, this list of conditions and the following disclaimer
1037535Sdes *    in this position and unchanged.
1137535Sdes * 2. Redistributions in binary form must reproduce the above copyright
1237535Sdes *    notice, this list of conditions and the following disclaimer in the
1337535Sdes *    documentation and/or other materials provided with the distribution.
1437535Sdes * 3. The name of the author may not be used to endorse or promote products
1537535Sdes *    derived from this software without specific prior written permission
1637535Sdes *
1737535Sdes * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
1837535Sdes * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
1937535Sdes * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
2037535Sdes * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
2137535Sdes * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
2237535Sdes * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
2337535Sdes * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
2437535Sdes * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
2537535Sdes * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
2637535Sdes * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2737535Sdes *
2841989Sdes *	$Id: ftp.c,v 1.11 1998/12/18 14:32:48 des Exp $
2937535Sdes */
3037535Sdes
3137535Sdes/*
3237571Sdes * Portions of this code were taken from or based on ftpio.c:
3337535Sdes *
3437535Sdes * ----------------------------------------------------------------------------
3537535Sdes * "THE BEER-WARE LICENSE" (Revision 42):
3637535Sdes * <phk@login.dknet.dk> wrote this file.  As long as you retain this notice you
3737535Sdes * can do whatever you want with this stuff. If we meet some day, and you think
3837535Sdes * this stuff is worth it, you can buy me a beer in return.   Poul-Henning Kamp
3937535Sdes * ----------------------------------------------------------------------------
4037535Sdes *
4137535Sdes * Major Changelog:
4237535Sdes *
4337535Sdes * Dag-Erling Co�dan Sm�rgrav
4437535Sdes * 9 Jun 1998
4537535Sdes *
4637535Sdes * Incorporated into libfetch
4737535Sdes *
4837535Sdes * Jordan K. Hubbard
4937535Sdes * 17 Jan 1996
5037535Sdes *
5137535Sdes * Turned inside out. Now returns xfers as new file ids, not as a special
5237535Sdes * `state' of FTP_t
5337535Sdes *
5437535Sdes * $ftpioId: ftpio.c,v 1.30 1998/04/11 07:28:53 phk Exp $
5537535Sdes *
5637535Sdes */
5737535Sdes
5841862Sdes#include <sys/param.h>
5937535Sdes#include <sys/socket.h>
6037535Sdes#include <netinet/in.h>
6137535Sdes
6237535Sdes#include <ctype.h>
6337573Sdes#include <stdarg.h>
6437535Sdes#include <stdio.h>
6537571Sdes#include <stdlib.h>
6637535Sdes#include <string.h>
6741869Sdes#include <time.h>
6837571Sdes#include <unistd.h>
6937535Sdes
7037535Sdes#include "fetch.h"
7140939Sdes#include "common.h"
7241862Sdes#include "ftperr.h"
7337535Sdes
7437535Sdes#define FTP_ANONYMOUS_USER	"ftp"
7537535Sdes#define FTP_ANONYMOUS_PASSWORD	"ftp"
7637573Sdes#define FTP_DEFAULT_PORT 21
7737535Sdes
7837573Sdes#define FTP_OPEN_DATA_CONNECTION	150
7937573Sdes#define FTP_OK				200
8041869Sdes#define FTP_FILE_STATUS			213
8141863Sdes#define FTP_SERVICE_READY		220
8237573Sdes#define FTP_PASSIVE_MODE		227
8337573Sdes#define FTP_LOGGED_IN			230
8437573Sdes#define FTP_FILE_ACTION_OK		250
8537573Sdes#define FTP_NEED_PASSWORD		331
8637573Sdes#define FTP_NEED_ACCOUNT		332
8737573Sdes
8837571Sdes#define ENDL "\r\n"
8937571Sdes
9040975Sdesstatic struct url cached_host;
9137535Sdesstatic FILE *cached_socket;
9237535Sdes
9341869Sdesstatic char _ftp_last_reply[1024];
9437571Sdes
9537571Sdes/*
9637535Sdes * Get server response, check that first digit is a '2'
9737535Sdes */
9837535Sdesstatic int
9941863Sdes_ftp_chkerr(FILE *s)
10037535Sdes{
10137535Sdes    char *line;
10237535Sdes    size_t len;
10337535Sdes
10437535Sdes    do {
10537573Sdes	if (((line = fgetln(s, &len)) == NULL) || (len < 4)) {
10640939Sdes	    _fetch_syserr();
10737535Sdes	    return -1;
10837571Sdes	}
10941869Sdes    } while (len >= 4 && line[3] == '-');
11037573Sdes
11141869Sdes    while (len && isspace(line[len-1]))
11241869Sdes	len--;
11341869Sdes    snprintf(_ftp_last_reply, sizeof(_ftp_last_reply),
11441869Sdes	     "%*.*s", (int)len, (int)len, line);
11537535Sdes
11637573Sdes#ifndef NDEBUG
11737573Sdes    fprintf(stderr, "\033[1m<<< ");
11841869Sdes    fprintf(stderr, "%*.*s\n", (int)len, (int)len, line);
11937573Sdes    fprintf(stderr, "\033[m");
12037573Sdes#endif
12137573Sdes
12241869Sdes    if (len < 4 || !isdigit(line[1]) || !isdigit(line[1])
12337571Sdes	|| !isdigit(line[2]) || (line[3] != ' ')) {
12437535Sdes	return -1;
12537571Sdes    }
12637535Sdes
12741863Sdes    return (line[0] - '0') * 100 + (line[1] - '0') * 10 + (line[2] - '0');
12837535Sdes}
12937535Sdes
13037535Sdes/*
13137573Sdes * Send a command and check reply
13237535Sdes */
13337535Sdesstatic int
13437573Sdes_ftp_cmd(FILE *f, char *fmt, ...)
13537535Sdes{
13637573Sdes    va_list ap;
13737573Sdes
13837573Sdes    va_start(ap, fmt);
13937573Sdes    vfprintf(f, fmt, ap);
14037573Sdes#ifndef NDEBUG
14137573Sdes    fprintf(stderr, "\033[1m>>> ");
14237573Sdes    vfprintf(stderr, fmt, ap);
14337573Sdes    fprintf(stderr, "\033[m");
14437573Sdes#endif
14537573Sdes    va_end(ap);
14637571Sdes
14741863Sdes    return _ftp_chkerr(f);
14837535Sdes}
14937535Sdes
15037535Sdes/*
15137608Sdes * Transfer file
15237535Sdes */
15337535Sdesstatic FILE *
15437608Sdes_ftp_transfer(FILE *cf, char *oper, char *file, char *mode, int pasv)
15537535Sdes{
15637573Sdes    struct sockaddr_in sin;
15741869Sdes    int e, sd = -1, l;
15837573Sdes    char *s;
15937573Sdes    FILE *df;
16037571Sdes
16137535Sdes    /* change directory */
16237573Sdes    if (((s = strrchr(file, '/')) != NULL) && (s != file)) {
16337573Sdes	*s = 0;
16441869Sdes	if ((e = _ftp_cmd(cf, "CWD %s" ENDL, file)) != FTP_FILE_ACTION_OK) {
16537573Sdes	    *s = '/';
16641869Sdes	    _ftp_seterr(e);
16737535Sdes	    return NULL;
16837535Sdes	}
16937573Sdes	*s++ = '/';
17037535Sdes    } else {
17141869Sdes	if ((e = _ftp_cmd(cf, "CWD /" ENDL)) != FTP_FILE_ACTION_OK) {
17241869Sdes	    _ftp_seterr(e);
17337535Sdes	    return NULL;
17441869Sdes	}
17537535Sdes    }
17637535Sdes
17737573Sdes    /* s now points to file name */
17837573Sdes
17937573Sdes    /* open data socket */
18038394Sdes    if ((sd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) {
18140939Sdes	_fetch_syserr();
18237573Sdes	return NULL;
18337573Sdes    }
18437573Sdes
18537573Sdes    if (pasv) {
18637573Sdes	u_char addr[6];
18737573Sdes	char *ln, *p;
18837573Sdes	int i;
18937573Sdes
19037573Sdes	/* send PASV command */
19141869Sdes	if ((e = _ftp_cmd(cf, "PASV" ENDL)) != FTP_PASSIVE_MODE)
19237573Sdes	    goto ouch;
19337573Sdes
19437573Sdes	/* find address and port number. The reply to the PASV command
19537573Sdes           is IMHO the one and only weak point in the FTP protocol. */
19637573Sdes	ln = _ftp_last_reply;
19737573Sdes	for (p = ln + 3; !isdigit(*p); p++)
19837573Sdes	    /* nothing */ ;
19937573Sdes	for (p--, i = 0; i < 6; i++) {
20037573Sdes	    p++; /* skip the comma */
20137573Sdes	    addr[i] = strtol(p, &p, 10);
20237573Sdes	}
20337573Sdes
20437573Sdes	/* construct sockaddr for data socket */
20537573Sdes	l = sizeof(sin);
20638394Sdes	if (getpeername(fileno(cf), (struct sockaddr *)&sin, &l) == -1)
20737573Sdes	    goto sysouch;
20837573Sdes	bcopy(addr, (char *)&sin.sin_addr, 4);
20937573Sdes	bcopy(addr + 4, (char *)&sin.sin_port, 2);
21037573Sdes
21137573Sdes	/* connect to data port */
21238394Sdes	if (connect(sd, (struct sockaddr *)&sin, sizeof(sin)) == -1)
21337573Sdes	    goto sysouch;
21437573Sdes
21537573Sdes	/* make the server initiate the transfer */
21641869Sdes	e = _ftp_cmd(cf, "%s %s" ENDL, oper, s);
21741869Sdes	if (e != FTP_OPEN_DATA_CONNECTION)
21837573Sdes	    goto ouch;
21937573Sdes
22037573Sdes    } else {
22137573Sdes	u_int32_t a;
22237573Sdes	u_short p;
22337573Sdes	int d;
22437573Sdes
22537573Sdes	/* find our own address, bind, and listen */
22637573Sdes	l = sizeof(sin);
22738394Sdes	if (getsockname(fileno(cf), (struct sockaddr *)&sin, &l) == -1)
22837573Sdes	    goto sysouch;
22937573Sdes	sin.sin_port = 0;
23038394Sdes	if (bind(sd, (struct sockaddr *)&sin, l) == -1)
23137573Sdes	    goto sysouch;
23238394Sdes	if (listen(sd, 1) == -1)
23337573Sdes	    goto sysouch;
23437573Sdes
23537573Sdes	/* find what port we're on and tell the server */
23638394Sdes	if (getsockname(sd, (struct sockaddr *)&sin, &l) == -1)
23737573Sdes	    goto sysouch;
23837573Sdes	a = ntohl(sin.sin_addr.s_addr);
23937573Sdes	p = ntohs(sin.sin_port);
24041869Sdes	e = _ftp_cmd(cf, "PORT %d,%d,%d,%d,%d,%d" ENDL,
24141869Sdes		     (a >> 24) & 0xff, (a >> 16) & 0xff,
24241869Sdes		     (a >> 8) & 0xff, a & 0xff,
24341869Sdes		     (p >> 8) & 0xff, p & 0xff);
24441869Sdes	if (e != FTP_OK)
24537573Sdes	    goto ouch;
24637573Sdes
24737573Sdes	/* make the server initiate the transfer */
24841869Sdes	e = _ftp_cmd(cf, "%s %s" ENDL, oper, s);
24941869Sdes	if (e != FTP_OPEN_DATA_CONNECTION)
25037573Sdes	    goto ouch;
25137573Sdes
25237573Sdes	/* accept the incoming connection and go to town */
25338394Sdes	if ((d = accept(sd, NULL, NULL)) == -1)
25437573Sdes	    goto sysouch;
25537573Sdes	close(sd);
25637573Sdes	sd = d;
25737573Sdes    }
25837573Sdes
25937608Sdes    if ((df = fdopen(sd, mode)) == NULL)
26037573Sdes	goto sysouch;
26137573Sdes    return df;
26237573Sdes
26337573Sdessysouch:
26440939Sdes    _fetch_syserr();
26541869Sdes    close(sd);
26641869Sdes    return NULL;
26741869Sdes
26837573Sdesouch:
26941869Sdes    _ftp_seterr(e);
27037573Sdes    close(sd);
27137535Sdes    return NULL;
27237535Sdes}
27337535Sdes
27437571Sdes/*
27537571Sdes * Log on to FTP server
27637535Sdes */
27737571Sdesstatic FILE *
27841862Sdes_ftp_connect(char *host, int port, char *user, char *pwd, int verbose)
27937571Sdes{
28037608Sdes    int sd, e, pp = FTP_DEFAULT_PORT;
28137608Sdes    char *p, *q;
28237571Sdes    FILE *f;
28337571Sdes
28437608Sdes    /* check for proxy */
28537608Sdes    if ((p = getenv("FTP_PROXY")) != NULL) {
28637608Sdes	if ((q = strchr(p, ':')) != NULL) {
28737608Sdes	    /* XXX check that it's a valid number */
28837608Sdes	    pp = atoi(q+1);
28937608Sdes	}
29037608Sdes	if (q)
29137608Sdes	    *q = 0;
29241923Sdes	sd = _fetch_connect(p, pp, verbose);
29337608Sdes	if (q)
29437608Sdes	    *q = ':';
29537608Sdes    } else {
29637608Sdes	/* no proxy, go straight to target */
29741923Sdes	sd = _fetch_connect(host, port, verbose);
29837608Sdes    }
29937608Sdes
30037608Sdes    /* check connection */
30138394Sdes    if (sd == -1) {
30240939Sdes	_fetch_syserr();
30337571Sdes	return NULL;
30437571Sdes    }
30537608Sdes
30637608Sdes    /* streams make life easier */
30737571Sdes    if ((f = fdopen(sd, "r+")) == NULL) {
30840939Sdes	_fetch_syserr();
30941869Sdes	close(sd);
31041869Sdes	return NULL;
31137571Sdes    }
31237571Sdes
31337571Sdes    /* expect welcome message */
31441869Sdes    if ((e = _ftp_chkerr(f)) != FTP_SERVICE_READY)
31537571Sdes	goto fouch;
31637571Sdes
31737571Sdes    /* send user name and password */
31837608Sdes    if (!user || !*user)
31937608Sdes	user = FTP_ANONYMOUS_USER;
32037608Sdes    e = p ? _ftp_cmd(f, "USER %s@%s@%d" ENDL, user, host, port)
32137608Sdes	  : _ftp_cmd(f, "USER %s" ENDL, user);
32237608Sdes
32337608Sdes    /* did the server request a password? */
32437608Sdes    if (e == FTP_NEED_PASSWORD) {
32537608Sdes	if (!pwd || !*pwd)
32637608Sdes	    pwd = FTP_ANONYMOUS_PASSWORD;
32737573Sdes	e = _ftp_cmd(f, "PASS %s" ENDL, pwd);
32837608Sdes    }
32937608Sdes
33037608Sdes    /* did the server request an account? */
33141869Sdes    if (e == FTP_NEED_ACCOUNT)
33241863Sdes	goto fouch;
33337608Sdes
33437608Sdes    /* we should be done by now */
33541869Sdes    if (e != FTP_LOGGED_IN)
33637571Sdes	goto fouch;
33737571Sdes
33837571Sdes    /* might as well select mode and type at once */
33937571Sdes#ifdef FTP_FORCE_STREAM_MODE
34041869Sdes    if ((e = _ftp_cmd(f, "MODE S" ENDL)) != FTP_OK) /* default is S */
34141869Sdes	goto fouch;
34237571Sdes#endif
34341869Sdes    if ((e = _ftp_cmd(f, "TYPE I" ENDL)) != FTP_OK) /* default is A */
34441869Sdes	goto fouch;
34537571Sdes
34637571Sdes    /* done */
34737571Sdes    return f;
34837571Sdes
34937571Sdesfouch:
35041869Sdes    _ftp_seterr(e);
35137571Sdes    fclose(f);
35237571Sdes    return NULL;
35337571Sdes}
35437571Sdes
35537571Sdes/*
35637571Sdes * Disconnect from server
35737571Sdes */
35837571Sdesstatic void
35937571Sdes_ftp_disconnect(FILE *f)
36037571Sdes{
36141863Sdes    (void)_ftp_cmd(f, "QUIT" ENDL);
36237571Sdes    fclose(f);
36337571Sdes}
36437571Sdes
36537571Sdes/*
36637571Sdes * Check if we're already connected
36737571Sdes */
36837571Sdesstatic int
36940975Sdes_ftp_isconnected(struct url *url)
37037571Sdes{
37137571Sdes    return (cached_socket
37237571Sdes	    && (strcmp(url->host, cached_host.host) == 0)
37337571Sdes	    && (strcmp(url->user, cached_host.user) == 0)
37437571Sdes	    && (strcmp(url->pwd, cached_host.pwd) == 0)
37537571Sdes	    && (url->port == cached_host.port));
37637571Sdes}
37737571Sdes
37837608Sdes/*
37941869Sdes * Check the cache, reconnect if no luck
38037608Sdes */
38137608Sdesstatic FILE *
38241869Sdes_ftp_cached_connect(struct url *url, char *flags)
38337535Sdes{
38441869Sdes    FILE *cf;
38537535Sdes
38641869Sdes    cf = NULL;
38741869Sdes
38837571Sdes    /* set default port */
38937571Sdes    if (!url->port)
39037573Sdes	url->port = FTP_DEFAULT_PORT;
39137535Sdes
39241863Sdes    /* try to use previously cached connection */
39341863Sdes    if (_ftp_isconnected(url))
39441869Sdes	if (_ftp_cmd(cached_socket, "NOOP" ENDL) != -1)
39537571Sdes	    cf = cached_socket;
39637571Sdes
39737571Sdes    /* connect to server */
39837571Sdes    if (!cf) {
39941862Sdes	cf = _ftp_connect(url->host, url->port, url->user, url->pwd,
40041862Sdes			  (strchr(flags, 'v') != NULL));
40137571Sdes	if (!cf)
40237571Sdes	    return NULL;
40337571Sdes	if (cached_socket)
40437571Sdes	    _ftp_disconnect(cached_socket);
40537571Sdes	cached_socket = cf;
40640975Sdes	memcpy(&cached_host, url, sizeof(struct url));
40737535Sdes    }
40837571Sdes
40941869Sdes    return cf;
41037535Sdes}
41137535Sdes
41237571Sdes/*
41341869Sdes * Get file
41437571Sdes */
41537535SdesFILE *
41640975SdesfetchGetFTP(struct url *url, char *flags)
41737608Sdes{
41841869Sdes    FILE *cf;
41941869Sdes
42041869Sdes    /* connect to server */
42141869Sdes    if ((cf = _ftp_cached_connect(url, flags)) == NULL)
42241869Sdes	return NULL;
42341869Sdes
42441869Sdes    /* initiate the transfer */
42541869Sdes    return _ftp_transfer(cf, "RETR", url->doc, "r",
42641869Sdes			 (flags && strchr(flags, 'p')));
42737608Sdes}
42837608Sdes
42941869Sdes/*
43041869Sdes * Put file
43141869Sdes */
43237608SdesFILE *
43340975SdesfetchPutFTP(struct url *url, char *flags)
43437535Sdes{
43541869Sdes    FILE *cf;
43641869Sdes
43741869Sdes    /* connect to server */
43841869Sdes    if ((cf = _ftp_cached_connect(url, flags)) == NULL)
43941869Sdes	return NULL;
44041869Sdes
44141869Sdes    /* initiate the transfer */
44241869Sdes    return _ftp_transfer(cf, (flags && strchr(flags, 'a')) ? "APPE" : "STOR",
44341869Sdes			 url->doc, "w", (flags && strchr(flags, 'p')));
44437535Sdes}
44540975Sdes
44641869Sdes/*
44741869Sdes * Get file stats
44841869Sdes */
44940975Sdesint
45040975SdesfetchStatFTP(struct url *url, struct url_stat *us, char *flags)
45140975Sdes{
45241869Sdes    FILE *cf;
45341869Sdes    char *ln, *s;
45441869Sdes    struct tm tm;
45541869Sdes    time_t t;
45641869Sdes    int e;
45741869Sdes
45841869Sdes    /* connect to server */
45941869Sdes    if ((cf = _ftp_cached_connect(url, flags)) == NULL)
46041869Sdes	return -1;
46141869Sdes
46241869Sdes    /* change directory */
46341869Sdes    if (((s = strrchr(url->doc, '/')) != NULL) && (s != url->doc)) {
46441869Sdes	*s = 0;
46541869Sdes	if ((e = _ftp_cmd(cf, "CWD %s" ENDL, url->doc)) != FTP_FILE_ACTION_OK) {
46641869Sdes	    *s = '/';
46741869Sdes	    goto ouch;
46841869Sdes	}
46941869Sdes	*s++ = '/';
47041869Sdes    } else {
47141869Sdes	if ((e = _ftp_cmd(cf, "CWD /" ENDL)) != FTP_FILE_ACTION_OK)
47241869Sdes	    goto ouch;
47341869Sdes    }
47441869Sdes
47541869Sdes    /* s now points to file name */
47641869Sdes
47741869Sdes    if (_ftp_cmd(cf, "SIZE %s" ENDL, s) != FTP_FILE_STATUS)
47841869Sdes	goto ouch;
47941869Sdes    for (ln = _ftp_last_reply + 4; *ln && isspace(*ln); ln++)
48041869Sdes	/* nothing */ ;
48141869Sdes    for (us->size = 0; *ln && isdigit(*ln); ln++)
48241869Sdes	us->size = us->size * 10 + *ln - '0';
48341869Sdes    if (*ln && !isspace(*ln)) {
48441869Sdes	_ftp_seterr(999); /* XXX should signal a FETCH_PROTO error */
48541869Sdes	return -1;
48641869Sdes    }
48741869Sdes
48841869Sdes    if ((e = _ftp_cmd(cf, "MDTM %s" ENDL, s)) != FTP_FILE_STATUS)
48941869Sdes	goto ouch;
49041869Sdes    for (ln = _ftp_last_reply + 4; *ln && isspace(*ln); ln++)
49141869Sdes	/* nothing */ ;
49241869Sdes    t = time(NULL);
49341869Sdes    us->mtime = localtime(&t)->tm_gmtoff;
49441869Sdes    sscanf(ln, "%04d%02d%02d%02d%02d%02d",
49541869Sdes	   &tm.tm_year, &tm.tm_mon, &tm.tm_mday,
49641869Sdes	   &tm.tm_hour, &tm.tm_min, &tm.tm_sec);
49741869Sdes    /* XXX should check the return value from sscanf */
49841869Sdes    tm.tm_mon--;
49941869Sdes    tm.tm_year -= 1900;
50041869Sdes    tm.tm_isdst = -1;
50141869Sdes    tm.tm_gmtoff = 0;
50241869Sdes    us->mtime += mktime(&tm);
50341869Sdes    us->atime = us->mtime;
50441869Sdes    return 0;
50541869Sdes
50641869Sdesouch:
50741869Sdes    _ftp_seterr(e);
50840975Sdes    return -1;
50940975Sdes}
51041989Sdes
51141989Sdes/*
51241989Sdes * List a directory
51341989Sdes */
51441989Sdesextern void warnx(char *, ...);
51541989Sdesstruct url_ent *
51641989SdesfetchListFTP(struct url *url, char *flags)
51741989Sdes{
51841989Sdes    warnx("fetchListFTP(): not implemented");
51941989Sdes    return NULL;
52041989Sdes}
521