xref: /openbsd-src/usr.bin/ftp/fetch.c (revision 298116df5b000b61a69743d21c92035418df8900)
1 /*	$OpenBSD: fetch.c,v 1.56 2005/08/05 21:01:53 fgsch Exp $	*/
2 /*	$NetBSD: fetch.c,v 1.14 1997/08/18 10:20:20 lukem Exp $	*/
3 
4 /*-
5  * Copyright (c) 1997 The NetBSD Foundation, Inc.
6  * All rights reserved.
7  *
8  * This code is derived from software contributed to The NetBSD Foundation
9  * by Jason Thorpe and Luke Mewburn.
10  *
11  * Redistribution and use in source and binary forms, with or without
12  * modification, are permitted provided that the following conditions
13  * are met:
14  * 1. Redistributions of source code must retain the above copyright
15  *    notice, this list of conditions and the following disclaimer.
16  * 2. Redistributions in binary form must reproduce the above copyright
17  *    notice, this list of conditions and the following disclaimer in the
18  *    documentation and/or other materials provided with the distribution.
19  * 3. All advertising materials mentioning features or use of this software
20  *    must display the following acknowledgement:
21  *        This product includes software developed by the NetBSD
22  *        Foundation, Inc. and its contributors.
23  * 4. Neither the name of The NetBSD Foundation nor the names of its
24  *    contributors may be used to endorse or promote products derived
25  *    from this software without specific prior written permission.
26  *
27  * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
28  * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
29  * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
30  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
31  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
32  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
33  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
34  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
35  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
36  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
37  * POSSIBILITY OF SUCH DAMAGE.
38  */
39 
40 #if !defined(lint) && !defined(SMALL)
41 static char rcsid[] = "$OpenBSD: fetch.c,v 1.56 2005/08/05 21:01:53 fgsch Exp $";
42 #endif /* not lint and not SMALL */
43 
44 /*
45  * FTP User Program -- Command line file retrieval
46  */
47 
48 #include <sys/types.h>
49 #include <sys/param.h>
50 #include <sys/socket.h>
51 #include <sys/stat.h>
52 
53 #include <netinet/in.h>
54 
55 #include <arpa/ftp.h>
56 #include <arpa/inet.h>
57 
58 #include <ctype.h>
59 #include <err.h>
60 #include <libgen.h>
61 #include <netdb.h>
62 #include <fcntl.h>
63 #include <signal.h>
64 #include <stdio.h>
65 #include <errno.h>
66 #include <stdlib.h>
67 #include <string.h>
68 #include <unistd.h>
69 #include <util.h>
70 
71 #include "ftp_var.h"
72 
73 static int	url_get(const char *, const char *, const char *);
74 void		aborthttp(int);
75 void		abortfile(int);
76 char		hextochar(const char *);
77 char		*urldecode(const char *);
78 
79 #define	FTP_URL		"ftp://"	/* ftp URL prefix */
80 #define	HTTP_URL	"http://"	/* http URL prefix */
81 #define	FILE_URL	"file:"		/* file URL prefix */
82 #define FTP_PROXY	"ftp_proxy"	/* env var with ftp proxy location */
83 #define HTTP_PROXY	"http_proxy"	/* env var with http proxy location */
84 
85 
86 #define EMPTYSTRING(x)	((x) == NULL || (*(x) == '\0'))
87 
88 static const char *at_encoding_warning =
89     "Extra `@' characters in usernames and passwords should be encoded as %%40";
90 
91 jmp_buf	httpabort;
92 
93 static int	redirect_loop;
94 
95 /*
96  * Retrieve URL, via the proxy in $proxyvar if necessary.
97  * Modifies the string argument given.
98  * Returns -1 on failure, 0 on success
99  */
100 static int
101 url_get(const char *origline, const char *proxyenv, const char *outfile)
102 {
103 	char pbuf[NI_MAXSERV], hbuf[NI_MAXHOST], *cp, *ep, *portnum, *path;
104 	char *hosttail, *cause = "unknown", *line, *host, *port, *buf = NULL;
105 	int error, i, isftpurl = 0, isfileurl = 0, isredirect = 0, rval = -1;
106 	struct addrinfo hints, *res0, *res;
107 	const char * volatile savefile;
108 	char * volatile proxy = NULL;
109 	volatile int s = -1, out;
110 	volatile sig_t oldintr;
111 	FILE *fin = NULL;
112 	off_t hashbytes;
113 	size_t len;
114 
115 	line = strdup(origline);
116 	if (line == NULL)
117 		errx(1, "Can't allocate memory to parse URL");
118 	if (strncasecmp(line, HTTP_URL, sizeof(HTTP_URL) - 1) == 0)
119 		host = line + sizeof(HTTP_URL) - 1;
120 	else if (strncasecmp(line, FTP_URL, sizeof(FTP_URL) - 1) == 0) {
121 		host = line + sizeof(FTP_URL) - 1;
122 		isftpurl = 1;
123 	} else if (strncasecmp(line, FILE_URL, sizeof(FILE_URL) - 1) == 0) {
124 		host = line + sizeof(FILE_URL) - 1;
125 		isfileurl = 1;
126 	} else
127 		errx(1, "url_get: Invalid URL '%s'", line);
128 
129 	if (isfileurl) {
130 		path = host;
131 	} else {
132 		path = strchr(host, '/');		/* find path */
133 		if (EMPTYSTRING(path)) {
134 			if (isftpurl)
135 				goto noftpautologin;
136 			warnx("Invalid URL (no `/' after host): %s", origline);
137 			goto cleanup_url_get;
138 		}
139 		*path++ = '\0';
140 		if (EMPTYSTRING(path)) {
141 			if (isftpurl)
142 				goto noftpautologin;
143 			warnx("Invalid URL (no file after host): %s", origline);
144 			goto cleanup_url_get;
145 		}
146 	}
147 
148 	if (outfile)
149 		savefile = outfile;
150 	else
151 		savefile = basename(path);
152 
153 	if (EMPTYSTRING(savefile)) {
154 		if (isftpurl)
155 			goto noftpautologin;
156 		warnx("Invalid URL (no file after directory): %s", origline);
157 		goto cleanup_url_get;
158 	}
159 
160 	if (proxyenv != NULL) {				/* use proxy */
161 		proxy = strdup(proxyenv);
162 		if (proxy == NULL)
163 			errx(1, "Can't allocate memory for proxy URL.");
164 		if (strncasecmp(proxy, HTTP_URL, sizeof(HTTP_URL) - 1) == 0)
165 			host = proxy + sizeof(HTTP_URL) - 1;
166 		else if (strncasecmp(proxy, FTP_URL, sizeof(FTP_URL) - 1) == 0)
167 			host = proxy + sizeof(FTP_URL) - 1;
168 		else {
169 			warnx("Malformed proxy URL: %s", proxyenv);
170 			goto cleanup_url_get;
171 		}
172 		if (EMPTYSTRING(host)) {
173 			warnx("Malformed proxy URL: %s", proxyenv);
174 			goto cleanup_url_get;
175 		}
176 		*--path = '/';			/* add / back to real path */
177 		path = strchr(host, '/');	/* remove trailing / on host */
178 		if (!EMPTYSTRING(path))
179 			*path++ = '\0';
180 		path = line;
181 	}
182 
183 	if (isfileurl) {
184 		struct stat st;
185 
186 		s = open(path, O_RDONLY);
187 		if (s == -1) {
188 			warn("Can't open file %s", path);
189 			goto cleanup_url_get;
190 		}
191 
192 		if (fstat(s, &st) == -1)
193 			filesize = -1;
194 		else
195 			filesize = st.st_size;
196 
197 		/* Open the output file.  */
198 		if (strcmp(savefile, "-") != 0) {
199 			out = open(savefile, O_CREAT | O_WRONLY | O_TRUNC,
200 			    0666);
201 			if (out < 0) {
202 				warn("Can't open %s", savefile);
203 				goto cleanup_url_get;
204 			}
205 		} else
206 			out = fileno(stdout);
207 
208 		/* Trap signals */
209 		oldintr = NULL;
210 		if (setjmp(httpabort)) {
211 			if (oldintr)
212 				(void)signal(SIGINT, oldintr);
213 			goto cleanup_url_get;
214 		}
215 		oldintr = signal(SIGINT, abortfile);
216 
217 		bytes = 0;
218 		hashbytes = mark;
219 		progressmeter(-1);
220 
221 		if ((buf = malloc(4096)) == NULL)
222 			errx(1, "Can't allocate memory for transfer buffer");
223 
224 		/* Finally, suck down the file. */
225 		i = 0;
226 		while ((len = read(s, buf, 4096)) > 0) {
227 			bytes += len;
228 			for (cp = buf; len > 0; len -= i, cp += i) {
229 				if ((i = write(out, cp, len)) == -1) {
230 					warn("Writing %s", savefile);
231 					goto cleanup_url_get;
232 				}
233 				else if (i == 0)
234 					break;
235 			}
236 			if (hash && !progress) {
237 				while (bytes >= hashbytes) {
238 					(void)putc('#', ttyout);
239 					hashbytes += mark;
240 				}
241 				(void)fflush(ttyout);
242 			}
243 		}
244 		if (hash && !progress && bytes > 0) {
245 			if (bytes < mark)
246 				(void)putc('#', ttyout);
247 			(void)putc('\n', ttyout);
248 			(void)fflush(ttyout);
249 		}
250 		if (len != 0) {
251 			warn("Reading from file");
252 			goto cleanup_url_get;
253 		}
254 		progressmeter(1);
255 		if (verbose)
256 			fputs("Successfully retrieved file.\n", ttyout);
257 		(void)signal(SIGINT, oldintr);
258 
259 		rval = 0;
260 		goto cleanup_url_get;
261 	}
262 
263 	if (*host == '[' && (hosttail = strrchr(host, ']')) != NULL &&
264 	    (hosttail[1] == '\0' || hosttail[1] == ':')) {
265 		host++;
266 		*hosttail++ = '\0';
267 	} else
268 		hosttail = host;
269 
270 	portnum = strrchr(hosttail, ':');		/* find portnum */
271 	if (portnum != NULL)
272 		*portnum++ = '\0';
273 
274 	if (debug)
275 		fprintf(ttyout, "host %s, port %s, path %s, save as %s.\n",
276 		    host, portnum, path, savefile);
277 
278 	memset(&hints, 0, sizeof(hints));
279 	hints.ai_family = family;
280 	hints.ai_socktype = SOCK_STREAM;
281 	port = portnum ? portnum : httpport;
282 	error = getaddrinfo(host, port, &hints, &res0);
283 	if (error == EAI_SERVICE && port == httpport) {
284 		/*
285 		 * If the services file is corrupt/missing, fall back
286 		 * on our hard-coded defines.
287 		 */
288 		snprintf(pbuf, sizeof(pbuf), "%d", HTTP_PORT);
289 		error = getaddrinfo(host, pbuf, &hints, &res0);
290 	}
291 	if (error) {
292 		warnx("%s: %s", gai_strerror(error), host);
293 		goto cleanup_url_get;
294 	}
295 
296 	s = -1;
297 	for (res = res0; res; res = res->ai_next) {
298 		if (getnameinfo(res->ai_addr, res->ai_addrlen, hbuf,
299 		    sizeof(hbuf), NULL, 0, NI_NUMERICHOST) != 0)
300 			strlcpy(hbuf, "(unknown)", sizeof(hbuf));
301 		if (verbose)
302 			fprintf(ttyout, "Trying %s...\n", hbuf);
303 
304 		s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
305 		if (s == -1) {
306 			cause = "socket";
307 			continue;
308 		}
309 
310 again:
311 		if (connect(s, res->ai_addr, res->ai_addrlen) < 0) {
312 			if (errno == EINTR)
313 				goto again;
314 			close(s);
315 			s = -1;
316 			cause = "connect";
317 			continue;
318 		}
319 
320 		/* get port in numeric */
321 		if (getnameinfo(res->ai_addr, res->ai_addrlen, NULL, 0,
322 		    pbuf, sizeof(pbuf), NI_NUMERICSERV) == 0)
323 			port = pbuf;
324 		else
325 			port = NULL;
326 
327 		break;
328 	}
329 	freeaddrinfo(res0);
330 	if (s < 0) {
331 		warn("%s", cause);
332 		goto cleanup_url_get;
333 	}
334 
335 	fin = fdopen(s, "r+");
336 
337 	if (verbose)
338 		fprintf(ttyout, "Requesting %s", origline);
339 	/*
340 	 * Construct and send the request. Proxy requests don't want leading /.
341 	 */
342 	if (proxy) {
343 		if (verbose)
344 			fprintf(ttyout, " (via %s)\n", proxyenv);
345 		/*
346 		 * Host: directive must use the destination host address for
347 		 * the original URI (path).  We do not attach it at this moment.
348 		 */
349 		fprintf(fin, "GET %s HTTP/1.0\r\n%s\r\n\r\n", path,
350 		    HTTP_USER_AGENT);
351 	} else {
352 		fprintf(fin, "GET /%s HTTP/1.0\r\nHost: ", path);
353 		if (strchr(host, ':')) {
354 			char *h, *p;
355 
356 			/*
357 			 * strip off scoped address portion, since it's
358 			 * local to node
359 			 */
360 			h = strdup(host);
361 			if (h == NULL)
362 				errx(1, "Can't allocate memory.");
363 			if ((p = strchr(h, '%')) != NULL)
364 				*p = '\0';
365 			fprintf(fin, "[%s]", h);
366 			free(h);
367 		} else
368 			fprintf(fin, "%s", host);
369 
370 		/*
371 		 * Send port number only if it's specified and does not equal
372 		 * 80. Some broken HTTP servers get confused if you explicitly
373 		 * send them the port number.
374 		 */
375 		if (port && strcmp(port, "80") != 0)
376 			fprintf(fin, ":%s", port);
377 		fprintf(fin, "\r\n%s\r\n\r\n", HTTP_USER_AGENT);
378 		if (verbose)
379 			fprintf(ttyout, "\n");
380 	}
381 	if (fflush(fin) == EOF) {
382 		warn("Writing HTTP request");
383 		goto cleanup_url_get;
384 	}
385 
386 	if ((buf = fparseln(fin, &len, NULL, "\0\0\0", 0)) == NULL) {
387 		warn("Receiving HTTP reply");
388 		goto cleanup_url_get;
389 	}
390 
391 	while (len > 0 && (buf[len-1] == '\r' || buf[len-1] == '\n'))
392 		buf[--len] = '\0';
393 	if (debug)
394 		fprintf(ttyout, "received '%s'\n", buf);
395 
396 	cp = strchr(buf, ' ');
397 	if (cp == NULL)
398 		goto improper;
399 	else
400 		cp++;
401 	if (strncmp(cp, "301", 3) == 0 || strncmp(cp, "302", 3) == 0) {
402 		isredirect++;
403 		if (redirect_loop++ > 10) {
404 			warnx("Too many redirections requested");
405 			goto cleanup_url_get;
406 		}
407 	} else if (strncmp(cp, "200", 3)) {
408 		warnx("Error retrieving file: %s", cp);
409 		goto cleanup_url_get;
410 	}
411 
412 	/*
413 	 * Read the rest of the header.
414 	 */
415 	free(buf);
416 	filesize = -1;
417 
418 	while (1) {
419 		if ((buf = fparseln(fin, &len, NULL, "\0\0\0", 0)) == NULL) {
420 			warn("Receiving HTTP reply");
421 			goto cleanup_url_get;
422 		}
423 		while (len > 0 && (buf[len-1] == '\r' || buf[len-1] == '\n'))
424 			buf[--len] = '\0';
425 		if (len == 0)
426 			break;
427 		if (debug)
428 			fprintf(ttyout, "received '%s'\n", buf);
429 
430 		/* Look for some headers */
431 		cp = buf;
432 #define CONTENTLEN "Content-Length: "
433 		if (strncasecmp(cp, CONTENTLEN, sizeof(CONTENTLEN) - 1) == 0) {
434 			cp += sizeof(CONTENTLEN) - 1;
435 			filesize = strtol(cp, &ep, 10);
436 			if (filesize < 1 || *ep != '\0')
437 				goto improper;
438 #define LOCATION "Location: "
439 		} else if (isredirect &&
440 		    strncasecmp(cp, LOCATION, sizeof(LOCATION) - 1) == 0) {
441 			cp += sizeof(LOCATION) - 1;
442 			if (verbose)
443 				fprintf(ttyout, "Redirected to %s\n", cp);
444 			if (fin != NULL)
445 				fclose(fin);
446 			else if (s != -1)
447 				close(s);
448 			if (proxy)
449 				free(proxy);
450 			free(line);
451 			rval = url_get(cp, proxyenv, outfile);
452 			if (buf)
453 				free(buf);
454 			return (rval);
455 		}
456 	}
457 
458 	/* Open the output file.  */
459 	if (strcmp(savefile, "-") != 0) {
460 		out = open(savefile, O_CREAT | O_WRONLY | O_TRUNC, 0666);
461 		if (out < 0) {
462 			warn("Can't open %s", savefile);
463 			goto cleanup_url_get;
464 		}
465 	} else
466 		out = fileno(stdout);
467 
468 	/* Trap signals */
469 	oldintr = NULL;
470 	if (setjmp(httpabort)) {
471 		if (oldintr)
472 			(void)signal(SIGINT, oldintr);
473 		goto cleanup_url_get;
474 	}
475 	oldintr = signal(SIGINT, aborthttp);
476 
477 	bytes = 0;
478 	hashbytes = mark;
479 	progressmeter(-1);
480 
481 	free(buf);
482 
483 	/* Finally, suck down the file. */
484 	if ((buf = malloc(4096)) == NULL)
485 		errx(1, "Can't allocate memory for transfer buffer");
486 	i = 0;
487 	while ((len = fread(buf, sizeof(char), 4096, fin)) > 0) {
488 		bytes += len;
489 		for (cp = buf; len > 0; len -= i, cp += i) {
490 			if ((i = write(out, cp, len)) == -1) {
491 				warn("Writing %s", savefile);
492 				goto cleanup_url_get;
493 			}
494 			else if (i == 0)
495 				break;
496 		}
497 		if (hash && !progress) {
498 			while (bytes >= hashbytes) {
499 				(void)putc('#', ttyout);
500 				hashbytes += mark;
501 			}
502 			(void)fflush(ttyout);
503 		}
504 	}
505 	if (hash && !progress && bytes > 0) {
506 		if (bytes < mark)
507 			(void)putc('#', ttyout);
508 		(void)putc('\n', ttyout);
509 		(void)fflush(ttyout);
510 	}
511 	if (len != 0) {
512 		warn("Reading from socket");
513 		goto cleanup_url_get;
514 	}
515 	progressmeter(1);
516 	if (filesize != -1 && len == 0 && bytes != filesize) {
517 		if (verbose)
518 			fputs("Read short file.\n", ttyout);
519 		goto cleanup_url_get;
520 	}
521 
522 	if (verbose)
523 		fputs("Successfully retrieved file.\n", ttyout);
524 	(void)signal(SIGINT, oldintr);
525 
526 	rval = 0;
527 	goto cleanup_url_get;
528 
529 noftpautologin:
530 	warnx(
531 	    "Auto-login using ftp URLs isn't supported when using $ftp_proxy");
532 	goto cleanup_url_get;
533 
534 improper:
535 	warnx("Improper response from %s", host);
536 
537 cleanup_url_get:
538 	if (fin != NULL)
539 		fclose(fin);
540 	else if (s != -1)
541 		close(s);
542 	if (buf)
543 		free(buf);
544 	if (proxy)
545 		free(proxy);
546 	free(line);
547 	return (rval);
548 }
549 
550 /*
551  * Abort a http retrieval
552  */
553 /* ARGSUSED */
554 void
555 aborthttp(int signo)
556 {
557 
558 	alarmtimer(0);
559 	fputs("\nhttp fetch aborted.\n", ttyout);
560 	(void)fflush(ttyout);
561 	longjmp(httpabort, 1);
562 }
563 
564 /*
565  * Abort a http retrieval
566  */
567 /* ARGSUSED */
568 void
569 abortfile(int signo)
570 {
571 
572 	alarmtimer(0);
573 	fputs("\nfile fetch aborted.\n", ttyout);
574 	(void)fflush(ttyout);
575 	longjmp(httpabort, 1);
576 }
577 
578 /*
579  * Retrieve multiple files from the command line, transferring
580  * files of the form "host:path", "ftp://host/path" using the
581  * ftp protocol, and files of the form "http://host/path" using
582  * the http protocol.
583  * If path has a trailing "/", then return (-1);
584  * the path will be cd-ed into and the connection remains open,
585  * and the function will return -1 (to indicate the connection
586  * is alive).
587  * If an error occurs the return value will be the offset+1 in
588  * argv[] of the file that caused a problem (i.e, argv[x]
589  * returns x+1)
590  * Otherwise, 0 is returned if all files retrieved successfully.
591  */
592 int
593 auto_fetch(int argc, char *argv[], char *outfile)
594 {
595 	char *xargv[5];
596 	char *cp, *line, *host, *dir, *file, *portnum;
597 	char *user, *pass, *pathstart;
598 	char *ftpproxy, *httpproxy;
599 	int rval, xargc;
600 	volatile int argpos;
601 	int dirhasglob, filehasglob, oautologin;
602 	char rempath[MAXPATHLEN];
603 
604 	argpos = 0;
605 
606 	if (setjmp(toplevel)) {
607 		if (connected)
608 			disconnect(0, NULL);
609 		return (argpos + 1);
610 	}
611 	(void)signal(SIGINT, (sig_t)intr);
612 	(void)signal(SIGPIPE, (sig_t)lostpeer);
613 
614 	if ((ftpproxy = getenv(FTP_PROXY)) != NULL && *ftpproxy == '\0')
615 		ftpproxy = NULL;
616 	if ((httpproxy = getenv(HTTP_PROXY)) != NULL && *httpproxy == '\0')
617 		httpproxy = NULL;
618 
619 	/*
620 	 * Loop through as long as there's files to fetch.
621 	 */
622 	for (rval = 0; (rval == 0) && (argpos < argc); free(line), argpos++) {
623 		if (strchr(argv[argpos], ':') == NULL)
624 			break;
625 		host = dir = file = portnum = user = pass = NULL;
626 
627 		/*
628 		 * We muck with the string, so we make a copy.
629 		 */
630 		line = strdup(argv[argpos]);
631 		if (line == NULL)
632 			errx(1, "Can't allocate memory for auto-fetch.");
633 
634 		/*
635 		 * Try HTTP URL-style arguments first.
636 		 */
637 		if (strncasecmp(line, HTTP_URL, sizeof(HTTP_URL) - 1) == 0 ||
638 		    strncasecmp(line, FILE_URL, sizeof(FILE_URL) - 1) == 0) {
639 			redirect_loop = 0;
640 			if (url_get(line, httpproxy, outfile) == -1)
641 				rval = argpos + 1;
642 			continue;
643 		}
644 
645 		/*
646 		 * Try FTP URL-style arguments next. If ftpproxy is
647 		 * set, use url_get() instead of standard ftp.
648 		 * Finally, try host:file.
649 		 */
650 		host = line;
651 		if (strncasecmp(line, FTP_URL, sizeof(FTP_URL) - 1) == 0) {
652 			char *passend, *passagain, *userend;
653 
654 			if (ftpproxy) {
655 				if (url_get(line, ftpproxy, outfile) == -1)
656 					rval = argpos + 1;
657 				continue;
658 			}
659 			host += sizeof(FTP_URL) - 1;
660 			dir = strchr(host, '/');
661 
662 			/* Look for [user:pass@]host[:port] */
663 
664 			/* check if we have "user:pass@" */
665 			userend = strchr(host, ':');
666 			passend = strchr(host, '@');
667 			if (passend && userend && userend < passend &&
668 			    (!dir || passend < dir)) {
669 				user = host;
670 				pass = userend + 1;
671 				host = passend + 1;
672 				*userend = *passend = '\0';
673 				passagain = strchr(host, '@');
674 				if (strchr(pass, '@') != NULL ||
675 				    (passagain != NULL && passagain < dir)) {
676 					warnx(at_encoding_warning);
677 					goto bad_ftp_url;
678 				}
679 
680 				if (EMPTYSTRING(user) || EMPTYSTRING(pass)) {
681 bad_ftp_url:
682 					warnx("Invalid URL: %s", argv[argpos]);
683 					rval = argpos + 1;
684 					continue;
685 				}
686 				user = urldecode(user);
687 				pass = urldecode(pass);
688 			}
689 
690 #ifdef INET6
691 			/* check [host]:port, or [host] */
692 			if (host[0] == '[') {
693 				cp = strchr(host, ']');
694 				if (cp && (!dir || cp < dir)) {
695 					if (cp + 1 == dir || cp[1] == ':') {
696 						host++;
697 						*cp++ = '\0';
698 					} else
699 						cp = NULL;
700 				} else
701 					cp = host;
702 			} else
703 				cp = host;
704 #else
705 			cp = host;
706 #endif
707 
708 			/* split off host[:port] if there is */
709 			if (cp) {
710 				portnum = strchr(cp, ':');
711 				pathstart = strchr(cp, '/');
712 				/* : in path is not a port # indicator */
713 				if (portnum && pathstart &&
714 				    pathstart < portnum)
715 					portnum = NULL;
716 
717 				if (!portnum)
718 					;
719 				else {
720 					if (!dir)
721 						;
722 					else if (portnum + 1 < dir) {
723 						*portnum++ = '\0';
724 						/*
725 						 * XXX should check if portnum
726 						 * is decimal number
727 						 */
728 					} else {
729 						/* empty portnum */
730 						goto bad_ftp_url;
731 					}
732 				}
733 			} else
734 				portnum = NULL;
735 		} else {			/* classic style `host:file' */
736 			dir = strchr(host, ':');
737 		}
738 		if (EMPTYSTRING(host)) {
739 			rval = argpos + 1;
740 			continue;
741 		}
742 
743 		/*
744 		 * If dir is NULL, the file wasn't specified
745 		 * (URL looked something like ftp://host)
746 		 */
747 		if (dir != NULL)
748 			*dir++ = '\0';
749 
750 		/*
751 		 * Extract the file and (if present) directory name.
752 		 */
753 		if (!EMPTYSTRING(dir)) {
754 			cp = strrchr(dir, '/');
755 			if (cp != NULL) {
756 				*cp++ = '\0';
757 				file = cp;
758 			} else {
759 				file = dir;
760 				dir = NULL;
761 			}
762 		}
763 		if (debug)
764 			fprintf(ttyout,
765 			    "user %s:%s host %s port %s dir %s file %s\n",
766 			    user, pass, host, portnum, dir, file);
767 
768 		/*
769 		 * Set up the connection.
770 		 */
771 		if (connected)
772 			disconnect(0, NULL);
773 		xargv[0] = __progname;
774 		xargv[1] = host;
775 		xargv[2] = NULL;
776 		xargc = 2;
777 		if (!EMPTYSTRING(portnum)) {
778 			xargv[2] = portnum;
779 			xargv[3] = NULL;
780 			xargc = 3;
781 		}
782 		oautologin = autologin;
783 		if (user != NULL)
784 			autologin = 0;
785 		setpeer(xargc, xargv);
786 		autologin = oautologin;
787 		if ((connected == 0) ||
788 		    ((connected == 1) && !ftp_login(host, user, pass))) {
789 			warnx("Can't connect or login to host `%s'", host);
790 			rval = argpos + 1;
791 			continue;
792 		}
793 
794 		/* Always use binary transfers. */
795 		setbinary(0, NULL);
796 
797 		dirhasglob = filehasglob = 0;
798 		if (doglob) {
799 			if (!EMPTYSTRING(dir) &&
800 			    strpbrk(dir, "*?[]{}") != NULL)
801 				dirhasglob = 1;
802 			if (!EMPTYSTRING(file) &&
803 			    strpbrk(file, "*?[]{}") != NULL)
804 				filehasglob = 1;
805 		}
806 
807 		/* Change directories, if necessary. */
808 		if (!EMPTYSTRING(dir) && !dirhasglob) {
809 			xargv[0] = "cd";
810 			xargv[1] = dir;
811 			xargv[2] = NULL;
812 			cd(2, xargv);
813 			if (!dirchange) {
814 				rval = argpos + 1;
815 				continue;
816 			}
817 		}
818 
819 		if (EMPTYSTRING(file)) {
820 			rval = -1;
821 			continue;
822 		}
823 
824 		if (verbose)
825 			fprintf(ttyout, "Retrieving %s/%s\n", dir ? dir : "", file);
826 
827 		if (dirhasglob) {
828 			snprintf(rempath, sizeof(rempath), "%s/%s", dir, file);
829 			file = rempath;
830 		}
831 
832 		/* Fetch the file(s). */
833 		xargc = 2;
834 		xargv[0] = "get";
835 		xargv[1] = file;
836 		xargv[2] = NULL;
837 		if (dirhasglob || filehasglob) {
838 			int ointeractive;
839 
840 			ointeractive = interactive;
841 			interactive = 0;
842 			xargv[0] = "mget";
843 			mget(xargc, xargv);
844 			interactive = ointeractive;
845 		} else {
846 			if (outfile != NULL) {
847 				xargv[2] = outfile;
848 				xargv[3] = NULL;
849 				xargc++;
850 			}
851 			get(xargc, xargv);
852 		}
853 
854 		if ((code / 100) != COMPLETE)
855 			rval = argpos + 1;
856 	}
857 	if (connected && rval != -1)
858 		disconnect(0, NULL);
859 	return (rval);
860 }
861 
862 char *
863 urldecode(const char *str)
864 {
865 	char *ret, c;
866 	int i, reallen;
867 
868 	if (str == NULL)
869 		return NULL;
870 	if ((ret = malloc(strlen(str)+1)) == NULL)
871 		err(1, "Can't allocate memory for URL decoding");
872 	for (i = 0, reallen = 0; str[i] != '\0'; i++, reallen++, ret++) {
873 		c = str[i];
874 		if (c == '+') {
875 			*ret = ' ';
876 			continue;
877 		}
878 		/* Can't use strtol here because next char after %xx may be
879 		 * a digit. */
880 		if (c == '%' && isxdigit(str[i+1]) && isxdigit(str[i+2])) {
881 			*ret = hextochar(&str[i+1]);
882 			i+=2;
883 			continue;
884 		}
885 		*ret = c;
886 	}
887 	*ret = '\0';
888 
889 	return ret-reallen;
890 }
891 
892 char
893 hextochar(const char *str)
894 {
895 	char c, ret;
896 
897 	c = str[0];
898 	ret = c;
899 	if (isalpha(c))
900 		ret -= isupper(c) ? 'A' - 10 : 'a' - 10;
901 	else
902 		ret -= '0';
903 	ret *= 16;
904 
905 	c = str[1];
906 	ret += c;
907 	if (isalpha(c))
908 		ret -= isupper(c) ? 'A' - 10 : 'a' - 10;
909 	else
910 		ret -= '0';
911 	return ret;
912 }
913 
914 int
915 isurl(const char *p)
916 {
917 
918 	if (strncasecmp(p, FTP_URL, sizeof(FTP_URL) - 1) == 0 ||
919 	    strncasecmp(p, HTTP_URL, sizeof(HTTP_URL) - 1) == 0 ||
920 	    strncasecmp(p, FILE_URL, sizeof(FILE_URL) - 1) == 0 ||
921 	    strstr(p, ":/"))
922 		return (1);
923 	return (0);
924 }
925