1 /* vi: set sw=4 ts=4: */
3 * wget - retrieve a file using HTTP or FTP
5 * Chip Rosenthal Covad Communications <chip@laserlink.net>
6 * Licensed under GPLv2, see file LICENSE in this source tree.
8 * Copyright (C) 2010 Bradley M. Kuhn <bkuhn@ebb.org>
9 * Kuhn's copyrights are licensed GPLv2-or-later. File as a whole remains GPLv2.
14 // May be used if we ever will want to free() all xstrdup()s...
15 /* char *allocated; */
26 off_t content_len; /* Content-length of the file */
27 off_t beg_range; /* Range at which continue begins */
28 #if ENABLE_FEATURE_WGET_STATUSBAR
29 off_t transferred; /* Number of bytes transferred so far */
30 const char *curfile; /* Name of current file being transferred */
33 #if ENABLE_FEATURE_WGET_TIMEOUT
34 unsigned timeout_seconds;
36 smallint chunked; /* chunked transfer encoding */
37 smallint got_clen; /* got content-length: from server */
38 /* Local downloads do benefit from big buffer.
39 * With 512 byte buffer, it was measured to be
40 * an order of magnitude slower than with big one.
42 uint64_t just_to_align_next_member;
43 char wget_buf[CONFIG_FEATURE_COPYBUF_KB*1024];
45 #define G (*ptr_to_globals)
46 #define INIT_G() do { \
47 SET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \
48 IF_FEATURE_WGET_TIMEOUT(G.timeout_seconds = 900;) \
52 /* Must match option string! */
54 WGET_OPT_CONTINUE = (1 << 0),
55 WGET_OPT_SPIDER = (1 << 1),
56 WGET_OPT_QUIET = (1 << 2),
57 WGET_OPT_OUTNAME = (1 << 3),
58 WGET_OPT_PREFIX = (1 << 4),
59 WGET_OPT_PROXY = (1 << 5),
60 WGET_OPT_USER_AGENT = (1 << 6),
61 WGET_OPT_NETWORK_READ_TIMEOUT = (1 << 7),
62 WGET_OPT_RETRIES = (1 << 8),
63 WGET_OPT_PASSIVE = (1 << 9),
64 WGET_OPT_HEADER = (1 << 10) * ENABLE_FEATURE_WGET_LONG_OPTIONS,
65 WGET_OPT_POST_DATA = (1 << 11) * ENABLE_FEATURE_WGET_LONG_OPTIONS,
73 #if ENABLE_FEATURE_WGET_STATUSBAR
74 static void progress_meter(int flag)
76 if (option_mask32 & WGET_OPT_QUIET)
79 if (flag == PROGRESS_START)
80 bb_progress_init(&G.pmt);
82 bb_progress_update(&G.pmt, G.curfile, G.beg_range, G.transferred,
83 G.chunked ? 0 : G.beg_range + G.transferred + G.content_len);
85 if (flag == PROGRESS_END) {
86 bb_putchar_stderr('\n');
91 static ALWAYS_INLINE void progress_meter(int flag UNUSED_PARAM) { }
95 /* IPv6 knows scoped address types i.e. link and site local addresses. Link
96 * local addresses can have a scope identifier to specify the
97 * interface/link an address is valid on (e.g. fe80::1%eth0). This scope
98 * identifier is only valid on a single node.
100 * RFC 4007 says that the scope identifier MUST NOT be sent across the wire,
101 * unless all nodes agree on the semantic. Apache e.g. regards zone identifiers
102 * in the Host header as invalid requests, see
103 * https://issues.apache.org/bugzilla/show_bug.cgi?id=35122
105 static void strip_ipv6_scope_id(char *host)
109 /* bbox wget actually handles IPv6 addresses without [], like
110 * wget "http://::1/xxx", but this is not standard.
111 * To save code, _here_ we do not support it. */
114 return; /* not IPv6 */
116 scope = strchr(host, '%');
120 /* Remove the IPv6 zone identifier from the host address */
121 cp = strchr(host, ']');
122 if (!cp || (cp[1] != ':' && cp[1] != '\0')) {
123 /* malformed address (not "[xx]:nn" or "[xx]") */
127 /* cp points to "]...", scope points to "%eth0]..." */
128 overlapping_strcpy(scope, cp);
131 #if 0 /* were needed when we used signal-driven progress bar */
132 /* Read NMEMB bytes into PTR from STREAM. Returns the number of bytes read,
133 * and a short count if an eof or non-interrupt error is encountered. */
134 static size_t safe_fread(void *ptr, size_t nmemb, FILE *stream)
137 char *p = (char*)ptr;
142 ret = fread(p, 1, nmemb, stream);
145 } while (nmemb && ferror(stream) && errno == EINTR);
147 return p - (char*)ptr;
150 /* Read a line or SIZE-1 bytes into S, whichever is less, from STREAM.
151 * Returns S, or NULL if an eof or non-interrupt error is encountered. */
152 static char *safe_fgets(char *s, int size, FILE *stream)
159 ret = fgets(s, size, stream);
160 } while (ret == NULL && ferror(stream) && errno == EINTR);
166 #if ENABLE_FEATURE_WGET_AUTHENTICATION
167 /* Base64-encode character string. */
168 static char *base64enc(const char *str)
170 unsigned len = strlen(str);
171 if (len > sizeof(G.wget_buf)/4*3 - 10) /* paranoia */
172 len = sizeof(G.wget_buf)/4*3 - 10;
173 bb_uuencode(G.wget_buf, str, len, bb_uuenc_tbl_base64);
178 static char* sanitize_string(char *s)
180 unsigned char *p = (void *) s;
187 static FILE *open_socket(len_and_sockaddr *lsa)
191 /* glibc 2.4 seems to try seeking on it - ??! */
192 /* hopefully it understands what ESPIPE means... */
193 fp = fdopen(xconnect_stream(lsa), "r+");
195 bb_perror_msg_and_die(bb_msg_memory_exhausted);
200 static int ftpcmd(const char *s1, const char *s2, FILE *fp)
205 fprintf(fp, "%s%s\r\n", s1, s2);
212 if (fgets(G.wget_buf, sizeof(G.wget_buf)-2, fp) == NULL) {
213 bb_perror_msg_and_die("error getting response");
215 buf_ptr = strstr(G.wget_buf, "\r\n");
219 } while (!isdigit(G.wget_buf[0]) || G.wget_buf[3] != ' ');
221 G.wget_buf[3] = '\0';
222 result = xatoi_positive(G.wget_buf);
227 static void parse_url(char *src_url, struct host_info *h)
231 /* h->allocated = */ url = xstrdup(src_url);
233 if (strncmp(url, "http://", 7) == 0) {
234 h->port = bb_lookup_port("http", "tcp", 80);
237 } else if (strncmp(url, "ftp://", 6) == 0) {
238 h->port = bb_lookup_port("ftp", "tcp", 21);
242 bb_error_msg_and_die("not an http or ftp url: %s", sanitize_string(url));
245 // "Real" wget 'http://busybox.net?var=a/b' sends this request:
246 // 'GET /?var=a/b HTTP 1.0'
247 // and saves 'index.html?var=a%2Fb' (we save 'b')
248 // wget 'http://busybox.net?login=john@doe':
249 // request: 'GET /?login=john@doe HTTP/1.0'
250 // saves: 'index.html?login=john@doe' (we save '?login=john@doe')
251 // wget 'http://busybox.net#test/test':
252 // request: 'GET / HTTP/1.0'
253 // saves: 'index.html' (we save 'test')
255 // We also don't add unique .N suffix if file exists...
256 sp = strchr(h->host, '/');
257 p = strchr(h->host, '?'); if (!sp || (p && sp > p)) sp = p;
258 p = strchr(h->host, '#'); if (!sp || (p && sp > p)) sp = p;
261 } else if (*sp == '/') {
264 } else { // '#' or '?'
265 // http://busybox.net?login=john@doe is a valid URL
266 // memmove converts to:
267 // http:/busybox.nett?login=john@doe...
268 memmove(h->host - 1, h->host, sp - h->host);
274 // We used to set h->user to NULL here, but this interferes
275 // with handling of code 302 ("object was moved")
277 sp = strrchr(h->host, '@');
287 static char *gethdr(FILE *fp /*, int *istrunc*/)
294 /* retrieve header line */
295 if (fgets(G.wget_buf, sizeof(G.wget_buf), fp) == NULL)
298 /* see if we are at the end of the headers */
299 for (s = G.wget_buf; *s == '\r'; ++s)
304 /* convert the header name to lower case */
305 for (s = G.wget_buf; isalnum(*s) || *s == '-' || *s == '.'; ++s) {
306 /* tolower for "A-Z", no-op for "0-9a-z-." */
310 /* verify we are at the end of the header name */
312 bb_error_msg_and_die("bad header line: %s", sanitize_string(G.wget_buf));
314 /* locate the start of the header value */
316 hdrval = skip_whitespace(s);
318 /* locate the end of header */
319 while (*s && *s != '\r' && *s != '\n')
322 /* end of header found */
328 /* Rats! The buffer isn't big enough to hold the entire header value */
329 while (c = getc(fp), c != EOF && c != '\n')
335 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
336 static char *URL_escape(const char *str)
338 /* URL encode, see RFC 2396 */
340 char *res = dst = xmalloc(strlen(str) * 3 + 1);
346 /* || strchr("!&'()*-.=_~", c) - more code */
358 || (c >= '0' && c <= '9')
359 || ((c|0x20) >= 'a' && (c|0x20) <= 'z')
366 *dst++ = bb_hexdigits_upcase[c >> 4];
367 *dst++ = bb_hexdigits_upcase[c & 0xf];
373 static FILE* prepare_ftp_session(FILE **dfpp, struct host_info *target, len_and_sockaddr *lsa)
380 target->user = xstrdup("anonymous:busybox@");
382 sfp = open_socket(lsa);
383 if (ftpcmd(NULL, NULL, sfp) != 220)
384 bb_error_msg_and_die("%s", sanitize_string(G.wget_buf + 4));
387 * Splitting username:password pair,
390 str = strchr(target->user, ':');
393 switch (ftpcmd("USER ", target->user, sfp)) {
397 if (ftpcmd("PASS ", str, sfp) == 230)
399 /* fall through (failed login) */
401 bb_error_msg_and_die("ftp login: %s", sanitize_string(G.wget_buf + 4));
404 ftpcmd("TYPE I", NULL, sfp);
409 if (ftpcmd("SIZE ", target->path, sfp) == 213) {
410 G.content_len = BB_STRTOOFF(G.wget_buf + 4, NULL, 10);
411 if (G.content_len < 0 || errno) {
412 bb_error_msg_and_die("SIZE value is garbage");
418 * Entering passive mode
420 if (ftpcmd("PASV", NULL, sfp) != 227) {
422 bb_error_msg_and_die("bad response to %s: %s", "PASV", sanitize_string(G.wget_buf));
424 // Response is "227 garbageN1,N2,N3,N4,P1,P2[)garbage]
425 // Server's IP is N1.N2.N3.N4 (we ignore it)
426 // Server's port for data connection is P1*256+P2
427 str = strrchr(G.wget_buf, ')');
428 if (str) str[0] = '\0';
429 str = strrchr(G.wget_buf, ',');
430 if (!str) goto pasv_error;
431 port = xatou_range(str+1, 0, 255);
433 str = strrchr(G.wget_buf, ',');
434 if (!str) goto pasv_error;
435 port += xatou_range(str+1, 0, 255) * 256;
436 set_nport(lsa, htons(port));
438 *dfpp = open_socket(lsa);
441 sprintf(G.wget_buf, "REST %"OFF_FMT"u", G.beg_range);
442 if (ftpcmd(G.wget_buf, NULL, sfp) == 350)
443 G.content_len -= G.beg_range;
446 if (ftpcmd("RETR ", target->path, sfp) > 150)
447 bb_error_msg_and_die("bad response to %s: %s", "RETR", sanitize_string(G.wget_buf));
452 static void NOINLINE retrieve_file_data(FILE *dfp, int output_fd)
454 #if ENABLE_FEATURE_WGET_STATUSBAR || ENABLE_FEATURE_WGET_TIMEOUT
455 # if ENABLE_FEATURE_WGET_TIMEOUT
458 struct pollfd polldata;
460 polldata.fd = fileno(dfp);
461 polldata.events = POLLIN | POLLPRI;
462 ndelay_on(polldata.fd);
464 progress_meter(PROGRESS_START);
469 /* Loops only if chunked */
475 rdsz = sizeof(G.wget_buf);
477 if (G.content_len < (off_t)sizeof(G.wget_buf)) {
478 if ((int)G.content_len <= 0)
480 rdsz = (unsigned)G.content_len;
483 #if ENABLE_FEATURE_WGET_STATUSBAR || ENABLE_FEATURE_WGET_TIMEOUT
484 # if ENABLE_FEATURE_WGET_TIMEOUT
485 second_cnt = G.timeout_seconds;
488 if (safe_poll(&polldata, 1, 1000) != 0)
489 break; /* error, EOF, or data is available */
490 # if ENABLE_FEATURE_WGET_TIMEOUT
491 if (second_cnt != 0 && --second_cnt == 0) {
492 progress_meter(PROGRESS_END);
493 bb_perror_msg_and_die("download timed out");
496 /* Needed for "stalled" indicator */
497 progress_meter(PROGRESS_BUMP);
500 n = fread(G.wget_buf, 1, rdsz, dfp);
503 /* perror will not work: ferror doesn't set errno */
504 bb_error_msg_and_die(bb_msg_read_error);
508 xwrite(output_fd, G.wget_buf, n);
509 #if ENABLE_FEATURE_WGET_STATUSBAR
511 progress_meter(PROGRESS_BUMP);
515 if (G.content_len == 0)
523 fgets(G.wget_buf, sizeof(G.wget_buf), dfp); /* This is a newline */
525 fgets(G.wget_buf, sizeof(G.wget_buf), dfp);
526 G.content_len = STRTOOFF(G.wget_buf, NULL, 16);
527 /* FIXME: error check? */
528 if (G.content_len == 0)
529 break; /* all done! */
533 progress_meter(PROGRESS_END);
536 int wget_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
537 int wget_main(int argc UNUSED_PARAM, char **argv)
539 struct host_info server, target;
540 len_and_sockaddr *lsa;
544 char *dir_prefix = NULL;
545 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
547 char *extra_headers = NULL;
548 llist_t *headers_llist = NULL;
550 FILE *sfp; /* socket to web/ftp server */
551 FILE *dfp; /* socket to ftp server (data) */
552 char *fname_out; /* where to direct output (-O) */
554 bool use_proxy; /* Use proxies if env vars are set */
555 const char *proxy_flag = "on"; /* Use proxies if env vars are set */
556 const char *user_agent = "Wget";/* "User-Agent" header field */
558 static const char keywords[] ALIGN1 =
559 "content-length\0""transfer-encoding\0""chunked\0""location\0";
561 KEY_content_length = 1, KEY_transfer_encoding, KEY_chunked, KEY_location
563 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
564 static const char wget_longopts[] ALIGN1 =
565 /* name, has_arg, val */
566 "continue\0" No_argument "c"
567 "spider\0" No_argument "s"
568 "quiet\0" No_argument "q"
569 "output-document\0" Required_argument "O"
570 "directory-prefix\0" Required_argument "P"
571 "proxy\0" Required_argument "Y"
572 "user-agent\0" Required_argument "U"
573 #if ENABLE_FEATURE_WGET_TIMEOUT
574 "timeout\0" Required_argument "T"
577 // "tries\0" Required_argument "t"
578 /* Ignored (we always use PASV): */
579 "passive-ftp\0" No_argument "\xff"
580 "header\0" Required_argument "\xfe"
581 "post-data\0" Required_argument "\xfd"
582 /* Ignored (we don't do ssl) */
583 "no-check-certificate\0" No_argument "\xfc"
589 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
590 applet_long_options = wget_longopts;
592 /* server.allocated = target.allocated = NULL; */
593 opt_complementary = "-1" IF_FEATURE_WGET_TIMEOUT(":T+") IF_FEATURE_WGET_LONG_OPTIONS(":\xfe::");
594 opt = getopt32(argv, "csqO:P:Y:U:T:" /*ignored:*/ "t:",
595 &fname_out, &dir_prefix,
596 &proxy_flag, &user_agent,
597 IF_FEATURE_WGET_TIMEOUT(&G.timeout_seconds) IF_NOT_FEATURE_WGET_TIMEOUT(NULL),
598 NULL /* -t RETRIES */
599 IF_FEATURE_WGET_LONG_OPTIONS(, &headers_llist)
600 IF_FEATURE_WGET_LONG_OPTIONS(, &post_data)
602 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
606 llist_t *ll = headers_llist;
608 size += strlen(ll->data) + 2;
611 extra_headers = cp = xmalloc(size);
612 while (headers_llist) {
613 cp += sprintf(cp, "%s\r\n", (char*)llist_pop(&headers_llist));
618 /* TODO: compat issue: should handle "wget URL1 URL2..." */
621 parse_url(argv[optind], &target);
623 /* Use the proxy if necessary */
624 use_proxy = (strcmp(proxy_flag, "off") != 0);
626 proxy = getenv(target.is_ftp ? "ftp_proxy" : "http_proxy");
627 if (proxy && proxy[0]) {
629 parse_url(proxy, &server);
635 server.port = target.port;
636 if (ENABLE_FEATURE_IPV6) {
637 server.host = xstrdup(target.host);
639 server.host = target.host;
643 if (ENABLE_FEATURE_IPV6)
644 strip_ipv6_scope_id(target.host);
646 /* Guess an output filename, if there was no -O FILE */
647 if (!(opt & WGET_OPT_OUTNAME)) {
648 fname_out = bb_get_last_path_component_nostrip(target.path);
649 /* handle "wget http://kernel.org//" */
650 if (fname_out[0] == '/' || !fname_out[0])
651 fname_out = (char*)"index.html";
652 /* -P DIR is considered only if there was no -O FILE */
654 fname_out = concat_path_file(dir_prefix, fname_out);
656 if (LONE_DASH(fname_out)) {
659 opt &= ~WGET_OPT_CONTINUE;
662 #if ENABLE_FEATURE_WGET_STATUSBAR
663 G.curfile = bb_get_last_path_component_nostrip(fname_out);
667 if ((opt & WGET_OPT_CONTINUE) && !fname_out)
668 bb_error_msg_and_die("can't specify continue (-c) without a filename (-O)");
671 /* Determine where to start transfer */
672 if (opt & WGET_OPT_CONTINUE) {
673 output_fd = open(fname_out, O_WRONLY);
674 if (output_fd >= 0) {
675 G.beg_range = xlseek(output_fd, 0, SEEK_END);
677 /* File doesn't exist. We do not create file here yet.
678 * We are not sure it exists on remove side */
683 lsa = xhost2sockaddr(server.host, server.port);
684 if (!(opt & WGET_OPT_QUIET)) {
685 char *s = xmalloc_sockaddr2dotted(&lsa->u.sa);
686 fprintf(stderr, "Connecting to %s (%s)\n", server.host, s);
690 if (use_proxy || !target.is_ftp) {
697 /* Open socket to http server */
698 sfp = open_socket(lsa);
700 /* Send HTTP request */
702 fprintf(sfp, "GET %stp://%s/%s HTTP/1.1\r\n",
703 target.is_ftp ? "f" : "ht", target.host,
706 if (opt & WGET_OPT_POST_DATA)
707 fprintf(sfp, "POST /%s HTTP/1.1\r\n", target.path);
709 fprintf(sfp, "GET /%s HTTP/1.1\r\n", target.path);
712 fprintf(sfp, "Host: %s\r\nUser-Agent: %s\r\n",
713 target.host, user_agent);
715 /* Ask server to close the connection as soon as we are done
716 * (IOW: we do not intend to send more requests)
718 fprintf(sfp, "Connection: close\r\n");
720 #if ENABLE_FEATURE_WGET_AUTHENTICATION
722 fprintf(sfp, "Proxy-Authorization: Basic %s\r\n"+6,
723 base64enc(target.user));
725 if (use_proxy && server.user) {
726 fprintf(sfp, "Proxy-Authorization: Basic %s\r\n",
727 base64enc(server.user));
732 fprintf(sfp, "Range: bytes=%"OFF_FMT"u-\r\n", G.beg_range);
734 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
736 fputs(extra_headers, sfp);
738 if (opt & WGET_OPT_POST_DATA) {
739 char *estr = URL_escape(post_data);
741 "Content-Type: application/x-www-form-urlencoded\r\n"
742 "Content-Length: %u\r\n"
745 (int) strlen(estr), estr
751 fprintf(sfp, "\r\n");
757 * Retrieve HTTP response line and check for "200" status code.
760 if (fgets(G.wget_buf, sizeof(G.wget_buf), sfp) == NULL)
761 bb_error_msg_and_die("no response from server");
764 str = skip_non_whitespace(str);
765 str = skip_whitespace(str);
766 // FIXME: no error check
767 // xatou wouldn't work: "200 OK"
772 while (gethdr(sfp /*, &n*/) != NULL)
773 /* eat all remaining headers */;
777 Response 204 doesn't say "null file", it says "metadata
778 has changed but data didn't":
780 "10.2.5 204 No Content
781 The server has fulfilled the request but does not need to return
782 an entity-body, and might want to return updated metainformation.
783 The response MAY include new or updated metainformation in the form
784 of entity-headers, which if present SHOULD be associated with
785 the requested variant.
787 If the client is a user agent, it SHOULD NOT change its document
788 view from that which caused the request to be sent. This response
789 is primarily intended to allow input for actions to take place
790 without causing a change to the user agent's active document view,
791 although any new or updated metainformation SHOULD be applied
792 to the document currently in the user agent's active view.
794 The 204 response MUST NOT include a message-body, and thus
795 is always terminated by the first empty line after the header fields."
797 However, in real world it was observed that some web servers
798 (e.g. Boa/0.94.14rc21) simply use code 204 when file size is zero.
802 case 300: /* redirection */
812 bb_error_msg_and_die("server returned error: %s", sanitize_string(G.wget_buf));
816 * Retrieve HTTP headers.
818 while ((str = gethdr(sfp /*, &n*/)) != NULL) {
819 /* gethdr converted "FOO:" string to lowercase */
821 /* strip trailing whitespace */
822 char *s = strchrnul(str, '\0') - 1;
823 while (s >= str && (*s == ' ' || *s == '\t')) {
827 key = index_in_strings(keywords, G.wget_buf) + 1;
828 if (key == KEY_content_length) {
829 G.content_len = BB_STRTOOFF(str, NULL, 10);
830 if (G.content_len < 0 || errno) {
831 bb_error_msg_and_die("content-length %s is garbage", sanitize_string(str));
836 if (key == KEY_transfer_encoding) {
837 if (index_in_strings(keywords, str_tolower(str)) + 1 != KEY_chunked)
838 bb_error_msg_and_die("transfer encoding '%s' is not supported", sanitize_string(str));
839 G.chunked = G.got_clen = 1;
841 if (key == KEY_location && status >= 300) {
842 if (--redir_limit == 0)
843 bb_error_msg_and_die("too many redirections");
848 /* free(target.allocated); */
849 target.path = /* target.allocated = */ xstrdup(str+1);
850 /* lsa stays the same: it's on the same server */
852 parse_url(str, &target);
854 server.host = target.host;
855 /* strip_ipv6_scope_id(target.host); - no! */
856 /* we assume remote never gives us IPv6 addr with scope id */
857 server.port = target.port;
860 } /* else: lsa stays the same: we use proxy */
862 goto establish_session;
865 // if (status >= 300)
866 // bb_error_msg_and_die("bad redirection (no Location: header from server)");
868 /* For HTTP, data is pumped over the same connection */
875 sfp = prepare_ftp_session(&dfp, &target, lsa);
878 if (opt & WGET_OPT_SPIDER) {
879 if (ENABLE_FEATURE_CLEAN_UP)
885 int o_flags = O_WRONLY | O_CREAT | O_TRUNC | O_EXCL;
886 /* compat with wget: -O FILE can overwrite */
887 if (opt & WGET_OPT_OUTNAME)
888 o_flags = O_WRONLY | O_CREAT | O_TRUNC;
889 output_fd = xopen(fname_out, o_flags);
892 retrieve_file_data(dfp, output_fd);
896 /* It's ftp. Close it properly */
898 if (ftpcmd(NULL, NULL, sfp) != 226)
899 bb_error_msg_and_die("ftp error: %s", sanitize_string(G.wget_buf + 4));
900 /* ftpcmd("QUIT", NULL, sfp); - why bother? */