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 /* Read NMEMB bytes into PTR from STREAM. Returns the number of bytes read,
132 * and a short count if an eof or non-interrupt error is encountered. */
133 static size_t safe_fread(void *ptr, size_t nmemb, FILE *stream)
136 char *p = (char*)ptr;
141 ret = fread(p, 1, nmemb, stream);
144 } while (nmemb && ferror(stream) && errno == EINTR);
146 return p - (char*)ptr;
149 /* Read a line or SIZE-1 bytes into S, whichever is less, from STREAM.
150 * Returns S, or NULL if an eof or non-interrupt error is encountered. */
151 static char *safe_fgets(char *s, int size, FILE *stream)
158 ret = fgets(s, size, stream);
159 } while (ret == NULL && ferror(stream) && errno == EINTR);
164 #if ENABLE_FEATURE_WGET_AUTHENTICATION
165 /* Base64-encode character string. */
166 static char *base64enc(const char *str)
168 unsigned len = strlen(str);
169 if (len > sizeof(G.wget_buf)/4*3 - 10) /* paranoia */
170 len = sizeof(G.wget_buf)/4*3 - 10;
171 bb_uuencode(G.wget_buf, str, len, bb_uuenc_tbl_base64);
176 static char* sanitize_string(char *s)
178 unsigned char *p = (void *) s;
185 static FILE *open_socket(len_and_sockaddr *lsa)
189 /* glibc 2.4 seems to try seeking on it - ??! */
190 /* hopefully it understands what ESPIPE means... */
191 fp = fdopen(xconnect_stream(lsa), "r+");
193 bb_perror_msg_and_die("fdopen");
198 static int ftpcmd(const char *s1, const char *s2, FILE *fp)
203 fprintf(fp, "%s%s\r\n", s1, s2);
210 if (fgets(G.wget_buf, sizeof(G.wget_buf)-2, fp) == NULL) {
211 bb_perror_msg_and_die("error getting response");
213 buf_ptr = strstr(G.wget_buf, "\r\n");
217 } while (!isdigit(G.wget_buf[0]) || G.wget_buf[3] != ' ');
219 G.wget_buf[3] = '\0';
220 result = xatoi_positive(G.wget_buf);
225 static void parse_url(char *src_url, struct host_info *h)
229 /* h->allocated = */ url = xstrdup(src_url);
231 if (strncmp(url, "http://", 7) == 0) {
232 h->port = bb_lookup_port("http", "tcp", 80);
235 } else if (strncmp(url, "ftp://", 6) == 0) {
236 h->port = bb_lookup_port("ftp", "tcp", 21);
240 bb_error_msg_and_die("not an http or ftp url: %s", sanitize_string(url));
243 // "Real" wget 'http://busybox.net?var=a/b' sends this request:
244 // 'GET /?var=a/b HTTP 1.0'
245 // and saves 'index.html?var=a%2Fb' (we save 'b')
246 // wget 'http://busybox.net?login=john@doe':
247 // request: 'GET /?login=john@doe HTTP/1.0'
248 // saves: 'index.html?login=john@doe' (we save '?login=john@doe')
249 // wget 'http://busybox.net#test/test':
250 // request: 'GET / HTTP/1.0'
251 // saves: 'index.html' (we save 'test')
253 // We also don't add unique .N suffix if file exists...
254 sp = strchr(h->host, '/');
255 p = strchr(h->host, '?'); if (!sp || (p && sp > p)) sp = p;
256 p = strchr(h->host, '#'); if (!sp || (p && sp > p)) sp = p;
259 } else if (*sp == '/') {
262 } else { // '#' or '?'
263 // http://busybox.net?login=john@doe is a valid URL
264 // memmove converts to:
265 // http:/busybox.nett?login=john@doe...
266 memmove(h->host - 1, h->host, sp - h->host);
272 // We used to set h->user to NULL here, but this interferes
273 // with handling of code 302 ("object was moved")
275 sp = strrchr(h->host, '@');
285 static char *gethdr(FILE *fp /*, int *istrunc*/)
292 /* retrieve header line */
293 if (fgets(G.wget_buf, sizeof(G.wget_buf), fp) == NULL)
296 /* see if we are at the end of the headers */
297 for (s = G.wget_buf; *s == '\r'; ++s)
302 /* convert the header name to lower case */
303 for (s = G.wget_buf; isalnum(*s) || *s == '-' || *s == '.'; ++s) {
304 /* tolower for "A-Z", no-op for "0-9a-z-." */
308 /* verify we are at the end of the header name */
310 bb_error_msg_and_die("bad header line: %s", sanitize_string(G.wget_buf));
312 /* locate the start of the header value */
314 hdrval = skip_whitespace(s);
316 /* locate the end of header */
317 while (*s && *s != '\r' && *s != '\n')
320 /* end of header found */
326 /* Rats! The buffer isn't big enough to hold the entire header value */
327 while (c = getc(fp), c != EOF && c != '\n')
333 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
334 static char *URL_escape(const char *str)
336 /* URL encode, see RFC 2396 */
338 char *res = dst = xmalloc(strlen(str) * 3 + 1);
344 /* || strchr("!&'()*-.=_~", c) - more code */
356 || (c >= '0' && c <= '9')
357 || ((c|0x20) >= 'a' && (c|0x20) <= 'z')
364 *dst++ = bb_hexdigits_upcase[c >> 4];
365 *dst++ = bb_hexdigits_upcase[c & 0xf];
371 static FILE* prepare_ftp_session(FILE **dfpp, struct host_info *target, len_and_sockaddr *lsa)
378 target->user = xstrdup("anonymous:busybox@");
380 sfp = open_socket(lsa);
381 if (ftpcmd(NULL, NULL, sfp) != 220)
382 bb_error_msg_and_die("%s", sanitize_string(G.wget_buf + 4));
385 * Splitting username:password pair,
388 str = strchr(target->user, ':');
391 switch (ftpcmd("USER ", target->user, sfp)) {
395 if (ftpcmd("PASS ", str, sfp) == 230)
397 /* fall through (failed login) */
399 bb_error_msg_and_die("ftp login: %s", sanitize_string(G.wget_buf + 4));
402 ftpcmd("TYPE I", NULL, sfp);
407 if (ftpcmd("SIZE ", target->path, sfp) == 213) {
408 G.content_len = BB_STRTOOFF(G.wget_buf + 4, NULL, 10);
409 if (G.content_len < 0 || errno) {
410 bb_error_msg_and_die("SIZE value is garbage");
416 * Entering passive mode
418 if (ftpcmd("PASV", NULL, sfp) != 227) {
420 bb_error_msg_and_die("bad response to %s: %s", "PASV", sanitize_string(G.wget_buf));
422 // Response is "227 garbageN1,N2,N3,N4,P1,P2[)garbage]
423 // Server's IP is N1.N2.N3.N4 (we ignore it)
424 // Server's port for data connection is P1*256+P2
425 str = strrchr(G.wget_buf, ')');
426 if (str) str[0] = '\0';
427 str = strrchr(G.wget_buf, ',');
428 if (!str) goto pasv_error;
429 port = xatou_range(str+1, 0, 255);
431 str = strrchr(G.wget_buf, ',');
432 if (!str) goto pasv_error;
433 port += xatou_range(str+1, 0, 255) * 256;
434 set_nport(lsa, htons(port));
436 *dfpp = open_socket(lsa);
439 sprintf(G.wget_buf, "REST %"OFF_FMT"u", G.beg_range);
440 if (ftpcmd(G.wget_buf, NULL, sfp) == 350)
441 G.content_len -= G.beg_range;
444 if (ftpcmd("RETR ", target->path, sfp) > 150)
445 bb_error_msg_and_die("bad response to %s: %s", "RETR", sanitize_string(G.wget_buf));
450 static void NOINLINE retrieve_file_data(FILE *dfp, int output_fd)
452 #if ENABLE_FEATURE_WGET_STATUSBAR || ENABLE_FEATURE_WGET_TIMEOUT
453 # if ENABLE_FEATURE_WGET_TIMEOUT
456 struct pollfd polldata;
458 polldata.fd = fileno(dfp);
459 polldata.events = POLLIN | POLLPRI;
460 ndelay_on(polldata.fd);
462 progress_meter(PROGRESS_START);
467 /* Loops only if chunked */
473 rdsz = sizeof(G.wget_buf);
475 if (G.content_len < (off_t)sizeof(G.wget_buf)) {
476 if ((int)G.content_len <= 0)
478 rdsz = (unsigned)G.content_len;
481 #if ENABLE_FEATURE_WGET_STATUSBAR || ENABLE_FEATURE_WGET_TIMEOUT
482 # if ENABLE_FEATURE_WGET_TIMEOUT
483 second_cnt = G.timeout_seconds;
486 if (safe_poll(&polldata, 1, 1000) != 0)
487 break; /* error, EOF, or data is available */
488 # if ENABLE_FEATURE_WGET_TIMEOUT
489 if (second_cnt != 0 && --second_cnt == 0) {
490 progress_meter(PROGRESS_END);
491 bb_perror_msg_and_die("download timed out");
494 /* Needed for "stalled" indicator */
495 progress_meter(PROGRESS_BUMP);
498 n = safe_fread(G.wget_buf, rdsz, dfp);
501 /* perror will not work: ferror doesn't set errno */
502 bb_error_msg_and_die(bb_msg_read_error);
506 xwrite(output_fd, G.wget_buf, n);
507 #if ENABLE_FEATURE_WGET_STATUSBAR
509 progress_meter(PROGRESS_BUMP);
518 safe_fgets(G.wget_buf, sizeof(G.wget_buf), dfp); /* This is a newline */
520 safe_fgets(G.wget_buf, sizeof(G.wget_buf), dfp);
521 G.content_len = STRTOOFF(G.wget_buf, NULL, 16);
522 /* FIXME: error check? */
523 if (G.content_len == 0)
524 break; /* all done! */
528 progress_meter(PROGRESS_END);
531 int wget_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
532 int wget_main(int argc UNUSED_PARAM, char **argv)
534 struct host_info server, target;
535 len_and_sockaddr *lsa;
539 char *dir_prefix = NULL;
540 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
542 char *extra_headers = NULL;
543 llist_t *headers_llist = NULL;
545 FILE *sfp; /* socket to web/ftp server */
546 FILE *dfp; /* socket to ftp server (data) */
547 char *fname_out; /* where to direct output (-O) */
549 bool use_proxy; /* Use proxies if env vars are set */
550 const char *proxy_flag = "on"; /* Use proxies if env vars are set */
551 const char *user_agent = "Wget";/* "User-Agent" header field */
553 static const char keywords[] ALIGN1 =
554 "content-length\0""transfer-encoding\0""chunked\0""location\0";
556 KEY_content_length = 1, KEY_transfer_encoding, KEY_chunked, KEY_location
558 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
559 static const char wget_longopts[] ALIGN1 =
560 /* name, has_arg, val */
561 "continue\0" No_argument "c"
562 "spider\0" No_argument "s"
563 "quiet\0" No_argument "q"
564 "output-document\0" Required_argument "O"
565 "directory-prefix\0" Required_argument "P"
566 "proxy\0" Required_argument "Y"
567 "user-agent\0" Required_argument "U"
568 #if ENABLE_FEATURE_WGET_TIMEOUT
569 "timeout\0" Required_argument "T"
572 // "tries\0" Required_argument "t"
573 /* Ignored (we always use PASV): */
574 "passive-ftp\0" No_argument "\xff"
575 "header\0" Required_argument "\xfe"
576 "post-data\0" Required_argument "\xfd"
577 /* Ignored (we don't do ssl) */
578 "no-check-certificate\0" No_argument "\xfc"
584 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
585 applet_long_options = wget_longopts;
587 /* server.allocated = target.allocated = NULL; */
588 opt_complementary = "-1" IF_FEATURE_WGET_TIMEOUT(":T+") IF_FEATURE_WGET_LONG_OPTIONS(":\xfe::");
589 opt = getopt32(argv, "csqO:P:Y:U:T:" /*ignored:*/ "t:",
590 &fname_out, &dir_prefix,
591 &proxy_flag, &user_agent,
592 IF_FEATURE_WGET_TIMEOUT(&G.timeout_seconds) IF_NOT_FEATURE_WGET_TIMEOUT(NULL),
593 NULL /* -t RETRIES */
594 IF_FEATURE_WGET_LONG_OPTIONS(, &headers_llist)
595 IF_FEATURE_WGET_LONG_OPTIONS(, &post_data)
597 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
601 llist_t *ll = headers_llist;
603 size += strlen(ll->data) + 2;
606 extra_headers = cp = xmalloc(size);
607 while (headers_llist) {
608 cp += sprintf(cp, "%s\r\n", (char*)llist_pop(&headers_llist));
613 /* TODO: compat issue: should handle "wget URL1 URL2..." */
616 parse_url(argv[optind], &target);
618 /* Use the proxy if necessary */
619 use_proxy = (strcmp(proxy_flag, "off") != 0);
621 proxy = getenv(target.is_ftp ? "ftp_proxy" : "http_proxy");
622 if (proxy && proxy[0]) {
624 parse_url(proxy, &server);
630 server.port = target.port;
631 if (ENABLE_FEATURE_IPV6) {
632 server.host = xstrdup(target.host);
634 server.host = target.host;
638 if (ENABLE_FEATURE_IPV6)
639 strip_ipv6_scope_id(target.host);
641 /* Guess an output filename, if there was no -O FILE */
642 if (!(opt & WGET_OPT_OUTNAME)) {
643 fname_out = bb_get_last_path_component_nostrip(target.path);
644 /* handle "wget http://kernel.org//" */
645 if (fname_out[0] == '/' || !fname_out[0])
646 fname_out = (char*)"index.html";
647 /* -P DIR is considered only if there was no -O FILE */
649 fname_out = concat_path_file(dir_prefix, fname_out);
651 if (LONE_DASH(fname_out)) {
654 opt &= ~WGET_OPT_CONTINUE;
657 #if ENABLE_FEATURE_WGET_STATUSBAR
658 G.curfile = bb_get_last_path_component_nostrip(fname_out);
662 if ((opt & WGET_OPT_CONTINUE) && !fname_out)
663 bb_error_msg_and_die("can't specify continue (-c) without a filename (-O)");
666 /* Determine where to start transfer */
667 if (opt & WGET_OPT_CONTINUE) {
668 output_fd = open(fname_out, O_WRONLY);
669 if (output_fd >= 0) {
670 G.beg_range = xlseek(output_fd, 0, SEEK_END);
672 /* File doesn't exist. We do not create file here yet.
673 * We are not sure it exists on remove side */
678 lsa = xhost2sockaddr(server.host, server.port);
679 if (!(opt & WGET_OPT_QUIET)) {
680 char *s = xmalloc_sockaddr2dotted(&lsa->u.sa);
681 fprintf(stderr, "Connecting to %s (%s)\n", server.host, s);
685 if (use_proxy || !target.is_ftp) {
692 /* Open socket to http server */
693 sfp = open_socket(lsa);
695 /* Send HTTP request */
697 fprintf(sfp, "GET %stp://%s/%s HTTP/1.1\r\n",
698 target.is_ftp ? "f" : "ht", target.host,
701 if (opt & WGET_OPT_POST_DATA)
702 fprintf(sfp, "POST /%s HTTP/1.1\r\n", target.path);
704 fprintf(sfp, "GET /%s HTTP/1.1\r\n", target.path);
707 fprintf(sfp, "Host: %s\r\nUser-Agent: %s\r\n",
708 target.host, user_agent);
710 #if ENABLE_FEATURE_WGET_AUTHENTICATION
712 fprintf(sfp, "Proxy-Authorization: Basic %s\r\n"+6,
713 base64enc(target.user));
715 if (use_proxy && server.user) {
716 fprintf(sfp, "Proxy-Authorization: Basic %s\r\n",
717 base64enc(server.user));
722 fprintf(sfp, "Range: bytes=%"OFF_FMT"u-\r\n", G.beg_range);
723 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
725 fputs(extra_headers, sfp);
727 if (opt & WGET_OPT_POST_DATA) {
728 char *estr = URL_escape(post_data);
729 fprintf(sfp, "Content-Type: application/x-www-form-urlencoded\r\n");
730 fprintf(sfp, "Content-Length: %u\r\n" "\r\n" "%s",
731 (int) strlen(estr), estr);
732 /*fprintf(sfp, "Connection: Keep-Alive\r\n\r\n");*/
733 /*fprintf(sfp, "%s\r\n", estr);*/
737 { /* If "Connection:" is needed, document why */
738 fprintf(sfp, /* "Connection: close\r\n" */ "\r\n");
744 * Retrieve HTTP response line and check for "200" status code.
747 if (fgets(G.wget_buf, sizeof(G.wget_buf), sfp) == NULL)
748 bb_error_msg_and_die("no response from server");
751 str = skip_non_whitespace(str);
752 str = skip_whitespace(str);
753 // FIXME: no error check
754 // xatou wouldn't work: "200 OK"
759 while (gethdr(sfp /*, &n*/) != NULL)
760 /* eat all remaining headers */;
764 Response 204 doesn't say "null file", it says "metadata
765 has changed but data didn't":
767 "10.2.5 204 No Content
768 The server has fulfilled the request but does not need to return
769 an entity-body, and might want to return updated metainformation.
770 The response MAY include new or updated metainformation in the form
771 of entity-headers, which if present SHOULD be associated with
772 the requested variant.
774 If the client is a user agent, it SHOULD NOT change its document
775 view from that which caused the request to be sent. This response
776 is primarily intended to allow input for actions to take place
777 without causing a change to the user agent's active document view,
778 although any new or updated metainformation SHOULD be applied
779 to the document currently in the user agent's active view.
781 The 204 response MUST NOT include a message-body, and thus
782 is always terminated by the first empty line after the header fields."
784 However, in real world it was observed that some web servers
785 (e.g. Boa/0.94.14rc21) simply use code 204 when file size is zero.
789 case 300: /* redirection */
799 bb_error_msg_and_die("server returned error: %s", sanitize_string(G.wget_buf));
803 * Retrieve HTTP headers.
805 while ((str = gethdr(sfp /*, &n*/)) != NULL) {
806 /* gethdr converted "FOO:" string to lowercase */
808 /* strip trailing whitespace */
809 char *s = strchrnul(str, '\0') - 1;
810 while (s >= str && (*s == ' ' || *s == '\t')) {
814 key = index_in_strings(keywords, G.wget_buf) + 1;
815 if (key == KEY_content_length) {
816 G.content_len = BB_STRTOOFF(str, NULL, 10);
817 if (G.content_len < 0 || errno) {
818 bb_error_msg_and_die("content-length %s is garbage", sanitize_string(str));
823 if (key == KEY_transfer_encoding) {
824 if (index_in_strings(keywords, str_tolower(str)) + 1 != KEY_chunked)
825 bb_error_msg_and_die("transfer encoding '%s' is not supported", sanitize_string(str));
826 G.chunked = G.got_clen = 1;
828 if (key == KEY_location && status >= 300) {
829 if (--redir_limit == 0)
830 bb_error_msg_and_die("too many redirections");
835 /* free(target.allocated); */
836 target.path = /* target.allocated = */ xstrdup(str+1);
837 /* lsa stays the same: it's on the same server */
839 parse_url(str, &target);
841 server.host = target.host;
842 /* strip_ipv6_scope_id(target.host); - no! */
843 /* we assume remote never gives us IPv6 addr with scope id */
844 server.port = target.port;
847 } /* else: lsa stays the same: we use proxy */
849 goto establish_session;
852 // if (status >= 300)
853 // bb_error_msg_and_die("bad redirection (no Location: header from server)");
855 /* For HTTP, data is pumped over the same connection */
862 sfp = prepare_ftp_session(&dfp, &target, lsa);
865 if (opt & WGET_OPT_SPIDER) {
866 if (ENABLE_FEATURE_CLEAN_UP)
872 int o_flags = O_WRONLY | O_CREAT | O_TRUNC | O_EXCL;
873 /* compat with wget: -O FILE can overwrite */
874 if (opt & WGET_OPT_OUTNAME)
875 o_flags = O_WRONLY | O_CREAT | O_TRUNC;
876 output_fd = xopen(fname_out, o_flags);
879 retrieve_file_data(dfp, output_fd);
883 /* It's ftp. Close it properly */
885 if (ftpcmd(NULL, NULL, sfp) != 226)
886 bb_error_msg_and_die("ftp error: %s", sanitize_string(G.wget_buf + 4));
887 /* ftpcmd("QUIT", NULL, sfp); - why bother? */