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("fdopen");
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);
520 fgets(G.wget_buf, sizeof(G.wget_buf), dfp); /* This is a newline */
522 fgets(G.wget_buf, sizeof(G.wget_buf), dfp);
523 G.content_len = STRTOOFF(G.wget_buf, NULL, 16);
524 /* FIXME: error check? */
525 if (G.content_len == 0)
526 break; /* all done! */
530 progress_meter(PROGRESS_END);
533 int wget_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
534 int wget_main(int argc UNUSED_PARAM, char **argv)
536 struct host_info server, target;
537 len_and_sockaddr *lsa;
541 char *dir_prefix = NULL;
542 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
544 char *extra_headers = NULL;
545 llist_t *headers_llist = NULL;
547 FILE *sfp; /* socket to web/ftp server */
548 FILE *dfp; /* socket to ftp server (data) */
549 char *fname_out; /* where to direct output (-O) */
551 bool use_proxy; /* Use proxies if env vars are set */
552 const char *proxy_flag = "on"; /* Use proxies if env vars are set */
553 const char *user_agent = "Wget";/* "User-Agent" header field */
555 static const char keywords[] ALIGN1 =
556 "content-length\0""transfer-encoding\0""chunked\0""location\0";
558 KEY_content_length = 1, KEY_transfer_encoding, KEY_chunked, KEY_location
560 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
561 static const char wget_longopts[] ALIGN1 =
562 /* name, has_arg, val */
563 "continue\0" No_argument "c"
564 "spider\0" No_argument "s"
565 "quiet\0" No_argument "q"
566 "output-document\0" Required_argument "O"
567 "directory-prefix\0" Required_argument "P"
568 "proxy\0" Required_argument "Y"
569 "user-agent\0" Required_argument "U"
570 #if ENABLE_FEATURE_WGET_TIMEOUT
571 "timeout\0" Required_argument "T"
574 // "tries\0" Required_argument "t"
575 /* Ignored (we always use PASV): */
576 "passive-ftp\0" No_argument "\xff"
577 "header\0" Required_argument "\xfe"
578 "post-data\0" Required_argument "\xfd"
579 /* Ignored (we don't do ssl) */
580 "no-check-certificate\0" No_argument "\xfc"
586 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
587 applet_long_options = wget_longopts;
589 /* server.allocated = target.allocated = NULL; */
590 opt_complementary = "-1" IF_FEATURE_WGET_TIMEOUT(":T+") IF_FEATURE_WGET_LONG_OPTIONS(":\xfe::");
591 opt = getopt32(argv, "csqO:P:Y:U:T:" /*ignored:*/ "t:",
592 &fname_out, &dir_prefix,
593 &proxy_flag, &user_agent,
594 IF_FEATURE_WGET_TIMEOUT(&G.timeout_seconds) IF_NOT_FEATURE_WGET_TIMEOUT(NULL),
595 NULL /* -t RETRIES */
596 IF_FEATURE_WGET_LONG_OPTIONS(, &headers_llist)
597 IF_FEATURE_WGET_LONG_OPTIONS(, &post_data)
599 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
603 llist_t *ll = headers_llist;
605 size += strlen(ll->data) + 2;
608 extra_headers = cp = xmalloc(size);
609 while (headers_llist) {
610 cp += sprintf(cp, "%s\r\n", (char*)llist_pop(&headers_llist));
615 /* TODO: compat issue: should handle "wget URL1 URL2..." */
618 parse_url(argv[optind], &target);
620 /* Use the proxy if necessary */
621 use_proxy = (strcmp(proxy_flag, "off") != 0);
623 proxy = getenv(target.is_ftp ? "ftp_proxy" : "http_proxy");
624 if (proxy && proxy[0]) {
626 parse_url(proxy, &server);
632 server.port = target.port;
633 if (ENABLE_FEATURE_IPV6) {
634 server.host = xstrdup(target.host);
636 server.host = target.host;
640 if (ENABLE_FEATURE_IPV6)
641 strip_ipv6_scope_id(target.host);
643 /* Guess an output filename, if there was no -O FILE */
644 if (!(opt & WGET_OPT_OUTNAME)) {
645 fname_out = bb_get_last_path_component_nostrip(target.path);
646 /* handle "wget http://kernel.org//" */
647 if (fname_out[0] == '/' || !fname_out[0])
648 fname_out = (char*)"index.html";
649 /* -P DIR is considered only if there was no -O FILE */
651 fname_out = concat_path_file(dir_prefix, fname_out);
653 if (LONE_DASH(fname_out)) {
656 opt &= ~WGET_OPT_CONTINUE;
659 #if ENABLE_FEATURE_WGET_STATUSBAR
660 G.curfile = bb_get_last_path_component_nostrip(fname_out);
664 if ((opt & WGET_OPT_CONTINUE) && !fname_out)
665 bb_error_msg_and_die("can't specify continue (-c) without a filename (-O)");
668 /* Determine where to start transfer */
669 if (opt & WGET_OPT_CONTINUE) {
670 output_fd = open(fname_out, O_WRONLY);
671 if (output_fd >= 0) {
672 G.beg_range = xlseek(output_fd, 0, SEEK_END);
674 /* File doesn't exist. We do not create file here yet.
675 * We are not sure it exists on remove side */
680 lsa = xhost2sockaddr(server.host, server.port);
681 if (!(opt & WGET_OPT_QUIET)) {
682 char *s = xmalloc_sockaddr2dotted(&lsa->u.sa);
683 fprintf(stderr, "Connecting to %s (%s)\n", server.host, s);
687 if (use_proxy || !target.is_ftp) {
694 /* Open socket to http server */
695 sfp = open_socket(lsa);
697 /* Send HTTP request */
699 fprintf(sfp, "GET %stp://%s/%s HTTP/1.1\r\n",
700 target.is_ftp ? "f" : "ht", target.host,
703 if (opt & WGET_OPT_POST_DATA)
704 fprintf(sfp, "POST /%s HTTP/1.1\r\n", target.path);
706 fprintf(sfp, "GET /%s HTTP/1.1\r\n", target.path);
709 fprintf(sfp, "Host: %s\r\nUser-Agent: %s\r\n",
710 target.host, user_agent);
712 #if ENABLE_FEATURE_WGET_AUTHENTICATION
714 fprintf(sfp, "Proxy-Authorization: Basic %s\r\n"+6,
715 base64enc(target.user));
717 if (use_proxy && server.user) {
718 fprintf(sfp, "Proxy-Authorization: Basic %s\r\n",
719 base64enc(server.user));
724 fprintf(sfp, "Range: bytes=%"OFF_FMT"u-\r\n", G.beg_range);
725 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
727 fputs(extra_headers, sfp);
729 if (opt & WGET_OPT_POST_DATA) {
730 char *estr = URL_escape(post_data);
731 fprintf(sfp, "Content-Type: application/x-www-form-urlencoded\r\n");
732 fprintf(sfp, "Content-Length: %u\r\n" "\r\n" "%s",
733 (int) strlen(estr), estr);
734 /*fprintf(sfp, "Connection: Keep-Alive\r\n\r\n");*/
735 /*fprintf(sfp, "%s\r\n", estr);*/
739 { /* If "Connection:" is needed, document why */
740 fprintf(sfp, /* "Connection: close\r\n" */ "\r\n");
746 * Retrieve HTTP response line and check for "200" status code.
749 if (fgets(G.wget_buf, sizeof(G.wget_buf), sfp) == NULL)
750 bb_error_msg_and_die("no response from server");
753 str = skip_non_whitespace(str);
754 str = skip_whitespace(str);
755 // FIXME: no error check
756 // xatou wouldn't work: "200 OK"
761 while (gethdr(sfp /*, &n*/) != NULL)
762 /* eat all remaining headers */;
766 Response 204 doesn't say "null file", it says "metadata
767 has changed but data didn't":
769 "10.2.5 204 No Content
770 The server has fulfilled the request but does not need to return
771 an entity-body, and might want to return updated metainformation.
772 The response MAY include new or updated metainformation in the form
773 of entity-headers, which if present SHOULD be associated with
774 the requested variant.
776 If the client is a user agent, it SHOULD NOT change its document
777 view from that which caused the request to be sent. This response
778 is primarily intended to allow input for actions to take place
779 without causing a change to the user agent's active document view,
780 although any new or updated metainformation SHOULD be applied
781 to the document currently in the user agent's active view.
783 The 204 response MUST NOT include a message-body, and thus
784 is always terminated by the first empty line after the header fields."
786 However, in real world it was observed that some web servers
787 (e.g. Boa/0.94.14rc21) simply use code 204 when file size is zero.
791 case 300: /* redirection */
801 bb_error_msg_and_die("server returned error: %s", sanitize_string(G.wget_buf));
805 * Retrieve HTTP headers.
807 while ((str = gethdr(sfp /*, &n*/)) != NULL) {
808 /* gethdr converted "FOO:" string to lowercase */
810 /* strip trailing whitespace */
811 char *s = strchrnul(str, '\0') - 1;
812 while (s >= str && (*s == ' ' || *s == '\t')) {
816 key = index_in_strings(keywords, G.wget_buf) + 1;
817 if (key == KEY_content_length) {
818 G.content_len = BB_STRTOOFF(str, NULL, 10);
819 if (G.content_len < 0 || errno) {
820 bb_error_msg_and_die("content-length %s is garbage", sanitize_string(str));
825 if (key == KEY_transfer_encoding) {
826 if (index_in_strings(keywords, str_tolower(str)) + 1 != KEY_chunked)
827 bb_error_msg_and_die("transfer encoding '%s' is not supported", sanitize_string(str));
828 G.chunked = G.got_clen = 1;
830 if (key == KEY_location && status >= 300) {
831 if (--redir_limit == 0)
832 bb_error_msg_and_die("too many redirections");
837 /* free(target.allocated); */
838 target.path = /* target.allocated = */ xstrdup(str+1);
839 /* lsa stays the same: it's on the same server */
841 parse_url(str, &target);
843 server.host = target.host;
844 /* strip_ipv6_scope_id(target.host); - no! */
845 /* we assume remote never gives us IPv6 addr with scope id */
846 server.port = target.port;
849 } /* else: lsa stays the same: we use proxy */
851 goto establish_session;
854 // if (status >= 300)
855 // bb_error_msg_and_die("bad redirection (no Location: header from server)");
857 /* For HTTP, data is pumped over the same connection */
864 sfp = prepare_ftp_session(&dfp, &target, lsa);
867 if (opt & WGET_OPT_SPIDER) {
868 if (ENABLE_FEATURE_CLEAN_UP)
874 int o_flags = O_WRONLY | O_CREAT | O_TRUNC | O_EXCL;
875 /* compat with wget: -O FILE can overwrite */
876 if (opt & WGET_OPT_OUTNAME)
877 o_flags = O_WRONLY | O_CREAT | O_TRUNC;
878 output_fd = xopen(fname_out, o_flags);
881 retrieve_file_data(dfp, output_fd);
885 /* It's ftp. Close it properly */
887 if (ftpcmd(NULL, NULL, sfp) != 226)
888 bb_error_msg_and_die("ftp error: %s", sanitize_string(G.wget_buf + 4));
889 /* ftpcmd("QUIT", NULL, sfp); - why bother? */