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