b40a1ac15e0ede5e9a5a133b57abd6504fd083fd
[oweals/busybox.git] / networking / wget.c
1 /* vi: set sw=4 ts=4: */
2 /*
3  * wget - retrieve a file using HTTP or FTP
4  *
5  * Chip Rosenthal Covad Communications <chip@laserlink.net>
6  *
7  * Licensed under GPLv2, see file LICENSE in this tarball for details.
8  */
9
10 #include "libbb.h"
11
12 struct host_info {
13         // May be used if we ever will want to free() all xstrdup()s...
14         /* char *allocated; */
15         const char *path;
16         const char *user;
17         char       *host;
18         int         port;
19         smallint    is_ftp;
20 };
21
22
23 /* Globals (can be accessed from signal handlers) */
24 struct globals {
25         off_t content_len;        /* Content-length of the file */
26         off_t beg_range;          /* Range at which continue begins */
27 #if ENABLE_FEATURE_WGET_STATUSBAR
28         off_t lastsize;
29         off_t totalsize;
30         off_t transferred;        /* Number of bytes transferred so far */
31         const char *curfile;      /* Name of current file being transferred */
32         unsigned lastupdate_sec;
33         unsigned start_sec;
34 #endif
35         smallint chunked;             /* chunked transfer encoding */
36 };
37 #define G (*(struct globals*)&bb_common_bufsiz1)
38 struct BUG_G_too_big {
39         char BUG_G_too_big[sizeof(G) <= COMMON_BUFSIZE ? 1 : -1];
40 };
41 #define content_len     (G.content_len    )
42 #define beg_range       (G.beg_range      )
43 #define lastsize        (G.lastsize       )
44 #define totalsize       (G.totalsize      )
45 #define transferred     (G.transferred    )
46 #define curfile         (G.curfile        )
47 #define lastupdate_sec  (G.lastupdate_sec )
48 #define start_sec       (G.start_sec      )
49 #define chunked         (G.chunked        )
50 #define INIT_G() do { } while (0)
51
52
53 #if ENABLE_FEATURE_WGET_STATUSBAR
54 enum {
55         STALLTIME = 5                   /* Seconds when xfer considered "stalled" */
56 };
57
58 static unsigned int get_tty2_width(void)
59 {
60         unsigned width;
61         get_terminal_width_height(2, &width, NULL);
62         return width;
63 }
64
65 static void progress_meter(int flag)
66 {
67         /* We can be called from signal handler */
68         int save_errno = errno;
69         off_t abbrevsize;
70         unsigned since_last_update, elapsed;
71         unsigned ratio;
72         int barlength, i;
73
74         if (flag == -1) { /* first call to progress_meter */
75                 start_sec = monotonic_sec();
76                 lastupdate_sec = start_sec;
77                 lastsize = 0;
78                 totalsize = content_len + beg_range; /* as content_len changes.. */
79         }
80
81         ratio = 100;
82         if (totalsize != 0 && !chunked) {
83                 /* long long helps to have it working even if !LFS */
84                 ratio = (unsigned) (100ULL * (transferred+beg_range) / totalsize);
85                 if (ratio > 100) ratio = 100;
86         }
87
88         fprintf(stderr, "\r%-20.20s%4d%% ", curfile, ratio);
89
90         barlength = get_tty2_width() - 49;
91         if (barlength > 0) {
92                 /* god bless gcc for variable arrays :) */
93                 i = barlength * ratio / 100;
94                 {
95                         char buf[i+1];
96                         memset(buf, '*', i);
97                         buf[i] = '\0';
98                         fprintf(stderr, "|%s%*s|", buf, barlength - i, "");
99                 }
100         }
101         i = 0;
102         abbrevsize = transferred + beg_range;
103         while (abbrevsize >= 100000) {
104                 i++;
105                 abbrevsize >>= 10;
106         }
107         /* see http://en.wikipedia.org/wiki/Tera */
108         fprintf(stderr, "%6d%c ", (int)abbrevsize, " kMGTPEZY"[i]);
109
110 // Nuts! Ain't it easier to update progress meter ONLY when we transferred++?
111
112         elapsed = monotonic_sec();
113         since_last_update = elapsed - lastupdate_sec;
114         if (transferred > lastsize) {
115                 lastupdate_sec = elapsed;
116                 lastsize = transferred;
117                 if (since_last_update >= STALLTIME) {
118                         /* We "cut off" these seconds from elapsed time
119                          * by adjusting start time */
120                         start_sec += since_last_update;
121                 }
122                 since_last_update = 0; /* we are un-stalled now */
123         }
124         elapsed -= start_sec; /* now it's "elapsed since start" */
125
126         if (since_last_update >= STALLTIME) {
127                 fprintf(stderr, " - stalled -");
128         } else {
129                 off_t to_download = totalsize - beg_range;
130                 if (transferred <= 0 || (int)elapsed <= 0 || transferred > to_download || chunked) {
131                         fprintf(stderr, "--:--:-- ETA");
132                 } else {
133                         /* to_download / (transferred/elapsed) - elapsed: */
134                         int eta = (int) ((unsigned long long)to_download*elapsed/transferred - elapsed);
135                         /* (long long helps to have working ETA even if !LFS) */
136                         i = eta % 3600;
137                         fprintf(stderr, "%02d:%02d:%02d ETA", eta / 3600, i / 60, i % 60);
138                 }
139         }
140
141         if (flag == 0) {
142                 /* last call to progress_meter */
143                 alarm(0);
144                 transferred = 0;
145                 fputc('\n', stderr);
146         } else {
147                 if (flag == -1) { /* first call to progress_meter */
148                         signal_SA_RESTART_empty_mask(SIGALRM, progress_meter);
149                 }
150                 alarm(1);
151         }
152
153         errno = save_errno;
154 }
155 /* Original copyright notice which applies to the CONFIG_FEATURE_WGET_STATUSBAR stuff,
156  * much of which was blatantly stolen from openssh.  */
157 /*-
158  * Copyright (c) 1992, 1993
159  *      The Regents of the University of California.  All rights reserved.
160  *
161  * Redistribution and use in source and binary forms, with or without
162  * modification, are permitted provided that the following conditions
163  * are met:
164  * 1. Redistributions of source code must retain the above copyright
165  *    notice, this list of conditions and the following disclaimer.
166  * 2. Redistributions in binary form must reproduce the above copyright
167  *    notice, this list of conditions and the following disclaimer in the
168  *    documentation and/or other materials provided with the distribution.
169  *
170  * 3. <BSD Advertising Clause omitted per the July 22, 1999 licensing change
171  *              ftp://ftp.cs.berkeley.edu/pub/4bsd/README.Impt.License.Change>
172  *
173  * 4. Neither the name of the University nor the names of its contributors
174  *    may be used to endorse or promote products derived from this software
175  *    without specific prior written permission.
176  *
177  * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
178  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
179  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
180  * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
181  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
182  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
183  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
184  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
185  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
186  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
187  * SUCH DAMAGE.
188  *
189  */
190 #else /* FEATURE_WGET_STATUSBAR */
191
192 static ALWAYS_INLINE void progress_meter(int flag UNUSED_PARAM) { }
193
194 #endif
195
196
197 /* Read NMEMB bytes into PTR from STREAM.  Returns the number of bytes read,
198  * and a short count if an eof or non-interrupt error is encountered.  */
199 static size_t safe_fread(void *ptr, size_t nmemb, FILE *stream)
200 {
201         size_t ret;
202         char *p = (char*)ptr;
203
204         do {
205                 clearerr(stream);
206                 errno = 0;
207                 ret = fread(p, 1, nmemb, stream);
208                 p += ret;
209                 nmemb -= ret;
210         } while (nmemb && ferror(stream) && errno == EINTR);
211
212         return p - (char*)ptr;
213 }
214
215 /* Read a line or SIZE-1 bytes into S, whichever is less, from STREAM.
216  * Returns S, or NULL if an eof or non-interrupt error is encountered.  */
217 static char *safe_fgets(char *s, int size, FILE *stream)
218 {
219         char *ret;
220
221         do {
222                 clearerr(stream);
223                 errno = 0;
224                 ret = fgets(s, size, stream);
225         } while (ret == NULL && ferror(stream) && errno == EINTR);
226
227         return ret;
228 }
229
230 #if ENABLE_FEATURE_WGET_AUTHENTICATION
231 /* Base64-encode character string. buf is assumed to be char buf[512]. */
232 static char *base64enc_512(char buf[512], const char *str)
233 {
234         unsigned len = strlen(str);
235         if (len > 512/4*3 - 10) /* paranoia */
236                 len = 512/4*3 - 10;
237         bb_uuencode(buf, str, len, bb_uuenc_tbl_base64);
238         return buf;
239 }
240 #endif
241
242
243 static FILE *open_socket(len_and_sockaddr *lsa)
244 {
245         FILE *fp;
246
247         /* glibc 2.4 seems to try seeking on it - ??! */
248         /* hopefully it understands what ESPIPE means... */
249         fp = fdopen(xconnect_stream(lsa), "r+");
250         if (fp == NULL)
251                 bb_perror_msg_and_die("fdopen");
252
253         return fp;
254 }
255
256
257 static int ftpcmd(const char *s1, const char *s2, FILE *fp, char *buf)
258 {
259         int result;
260         if (s1) {
261                 if (!s2) s2 = "";
262                 fprintf(fp, "%s%s\r\n", s1, s2);
263                 fflush(fp);
264         }
265
266         do {
267                 char *buf_ptr;
268
269                 if (fgets(buf, 510, fp) == NULL) {
270                         bb_perror_msg_and_die("error getting response");
271                 }
272                 buf_ptr = strstr(buf, "\r\n");
273                 if (buf_ptr) {
274                         *buf_ptr = '\0';
275                 }
276         } while (!isdigit(buf[0]) || buf[3] != ' ');
277
278         buf[3] = '\0';
279         result = xatoi_u(buf);
280         buf[3] = ' ';
281         return result;
282 }
283
284
285 static void parse_url(char *src_url, struct host_info *h)
286 {
287         char *url, *p, *sp;
288
289         /* h->allocated = */ url = xstrdup(src_url);
290
291         if (strncmp(url, "http://", 7) == 0) {
292                 h->port = bb_lookup_port("http", "tcp", 80);
293                 h->host = url + 7;
294                 h->is_ftp = 0;
295         } else if (strncmp(url, "ftp://", 6) == 0) {
296                 h->port = bb_lookup_port("ftp", "tcp", 21);
297                 h->host = url + 6;
298                 h->is_ftp = 1;
299         } else
300                 bb_error_msg_and_die("not an http or ftp url: %s", url);
301
302         // FYI:
303         // "Real" wget 'http://busybox.net?var=a/b' sends this request:
304         //   'GET /?var=a/b HTTP 1.0'
305         //   and saves 'index.html?var=a%2Fb' (we save 'b')
306         // wget 'http://busybox.net?login=john@doe':
307         //   request: 'GET /?login=john@doe HTTP/1.0'
308         //   saves: 'index.html?login=john@doe' (we save '?login=john@doe')
309         // wget 'http://busybox.net#test/test':
310         //   request: 'GET / HTTP/1.0'
311         //   saves: 'index.html' (we save 'test')
312         //
313         // We also don't add unique .N suffix if file exists...
314         sp = strchr(h->host, '/');
315         p = strchr(h->host, '?'); if (!sp || (p && sp > p)) sp = p;
316         p = strchr(h->host, '#'); if (!sp || (p && sp > p)) sp = p;
317         if (!sp) {
318                 h->path = "";
319         } else if (*sp == '/') {
320                 *sp = '\0';
321                 h->path = sp + 1;
322         } else { // '#' or '?'
323                 // http://busybox.net?login=john@doe is a valid URL
324                 // memmove converts to:
325                 // http:/busybox.nett?login=john@doe...
326                 memmove(h->host - 1, h->host, sp - h->host);
327                 h->host--;
328                 sp[-1] = '\0';
329                 h->path = sp;
330         }
331
332         sp = strrchr(h->host, '@');
333         h->user = NULL;
334         if (sp != NULL) {
335                 h->user = h->host;
336                 *sp = '\0';
337                 h->host = sp + 1;
338         }
339
340         sp = h->host;
341 }
342
343
344 static char *gethdr(char *buf, size_t bufsiz, FILE *fp /*, int *istrunc*/)
345 {
346         char *s, *hdrval;
347         int c;
348
349         /* *istrunc = 0; */
350
351         /* retrieve header line */
352         if (fgets(buf, bufsiz, fp) == NULL)
353                 return NULL;
354
355         /* see if we are at the end of the headers */
356         for (s = buf; *s == '\r'; ++s)
357                 continue;
358         if (*s == '\n')
359                 return NULL;
360
361         /* convert the header name to lower case */
362         for (s = buf; isalnum(*s) || *s == '-' || *s == '.'; ++s)
363                 *s = tolower(*s);
364
365         /* verify we are at the end of the header name */
366         if (*s != ':')
367                 bb_error_msg_and_die("bad header line: %s", buf);
368
369         /* locate the start of the header value */
370         *s++ = '\0';
371         hdrval = skip_whitespace(s);
372
373         /* locate the end of header */
374         while (*s && *s != '\r' && *s != '\n')
375                 ++s;
376
377         /* end of header found */
378         if (*s) {
379                 *s = '\0';
380                 return hdrval;
381         }
382
383         /* Rats! The buffer isn't big enough to hold the entire header value. */
384         while (c = getc(fp), c != EOF && c != '\n')
385                 continue;
386         /* *istrunc = 1; */
387         return hdrval;
388 }
389
390
391 int wget_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
392 int wget_main(int argc UNUSED_PARAM, char **argv)
393 {
394         char buf[512];
395         struct host_info server, target;
396         len_and_sockaddr *lsa;
397         int status;
398         int port;
399         int try = 5;
400         unsigned opt;
401         char *str;
402         char *proxy = 0;
403         char *dir_prefix = NULL;
404 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
405         char *extra_headers = NULL;
406         llist_t *headers_llist = NULL;
407 #endif
408         FILE *sfp = NULL;               /* socket to web/ftp server         */
409         FILE *dfp;                      /* socket to ftp server (data)      */
410         char *fname_out;                /* where to direct output (-O)      */
411         bool got_clen = 0;              /* got content-length: from server  */
412         int output_fd = -1;
413         bool use_proxy = 1;             /* Use proxies if env vars are set  */
414         const char *proxy_flag = "on";  /* Use proxies if env vars are set  */
415         const char *user_agent = "Wget";/* "User-Agent" header field        */
416
417         static const char keywords[] ALIGN1 =
418                 "content-length\0""transfer-encoding\0""chunked\0""location\0";
419         enum {
420                 KEY_content_length = 1, KEY_transfer_encoding, KEY_chunked, KEY_location
421         };
422         enum {
423                 WGET_OPT_CONTINUE   = 0x1,
424                 WGET_OPT_SPIDER     = 0x2,
425                 WGET_OPT_QUIET      = 0x4,
426                 WGET_OPT_OUTNAME    = 0x8,
427                 WGET_OPT_PREFIX     = 0x10,
428                 WGET_OPT_PROXY      = 0x20,
429                 WGET_OPT_USER_AGENT = 0x40,
430                 WGET_OPT_PASSIVE    = 0x80,
431                 WGET_OPT_HEADER     = 0x100,
432         };
433 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
434         static const char wget_longopts[] ALIGN1 =
435                 /* name, has_arg, val */
436                 "continue\0"         No_argument       "c"
437                 "spider\0"           No_argument       "s"
438                 "quiet\0"            No_argument       "q"
439                 "output-document\0"  Required_argument "O"
440                 "directory-prefix\0" Required_argument "P"
441                 "proxy\0"            Required_argument "Y"
442                 "user-agent\0"       Required_argument "U"
443                 "passive-ftp\0"      No_argument       "\xff"
444                 "header\0"           Required_argument "\xfe"
445                 ;
446 #endif
447
448         INIT_G();
449
450 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
451         applet_long_options = wget_longopts;
452 #endif
453         /* server.allocated = target.allocated = NULL; */
454         opt_complementary = "-1" USE_FEATURE_WGET_LONG_OPTIONS(":\xfe::");
455         opt = getopt32(argv, "csqO:P:Y:U:" /*ignored:*/ "t:T:",
456                                 &fname_out, &dir_prefix,
457                                 &proxy_flag, &user_agent,
458                                 NULL, /* -t RETRIES */
459                                 NULL /* -T NETWORK_READ_TIMEOUT */
460                                 USE_FEATURE_WGET_LONG_OPTIONS(, &headers_llist)
461                                 );
462         if (strcmp(proxy_flag, "off") == 0) {
463                 /* Use the proxy if necessary */
464                 use_proxy = 0;
465         }
466 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
467         if (headers_llist) {
468                 int size = 1;
469                 char *cp;
470                 llist_t *ll = headers_llist;
471                 while (ll) {
472                         size += strlen(ll->data) + 2;
473                         ll = ll->link;
474                 }
475                 extra_headers = cp = xmalloc(size);
476                 while (headers_llist) {
477                         cp += sprintf(cp, "%s\r\n", (char*)llist_pop(&headers_llist));
478                 }
479         }
480 #endif
481
482         parse_url(argv[optind], &target);
483         server.host = target.host;
484         server.port = target.port;
485
486         /* Use the proxy if necessary */
487         if (use_proxy) {
488                 proxy = getenv(target.is_ftp ? "ftp_proxy" : "http_proxy");
489                 if (proxy && *proxy) {
490                         parse_url(proxy, &server);
491                 } else {
492                         use_proxy = 0;
493                 }
494         }
495
496         /* Guess an output filename, if there was no -O FILE */
497         if (!(opt & WGET_OPT_OUTNAME)) {
498                 fname_out = bb_get_last_path_component_nostrip(target.path);
499                 /* handle "wget http://kernel.org//" */
500                 if (fname_out[0] == '/' || !fname_out[0])
501                         fname_out = (char*)"index.html";
502                 /* -P DIR is considered only if there was no -O FILE */
503                 if (dir_prefix)
504                         fname_out = concat_path_file(dir_prefix, fname_out);
505         } else {
506                 if (LONE_DASH(fname_out)) {
507                         /* -O - */
508                         output_fd = 1;
509                         opt &= ~WGET_OPT_CONTINUE;
510                 }
511         }
512 #if ENABLE_FEATURE_WGET_STATUSBAR
513         curfile = bb_get_last_path_component_nostrip(fname_out);
514 #endif
515
516         /* Impossible?
517         if ((opt & WGET_OPT_CONTINUE) && !fname_out)
518                 bb_error_msg_and_die("cannot specify continue (-c) without a filename (-O)"); */
519
520         /* Determine where to start transfer */
521         if (opt & WGET_OPT_CONTINUE) {
522                 output_fd = open(fname_out, O_WRONLY);
523                 if (output_fd >= 0) {
524                         beg_range = xlseek(output_fd, 0, SEEK_END);
525                 }
526                 /* File doesn't exist. We do not create file here yet.
527                    We are not sure it exists on remove side */
528         }
529
530         /* We want to do exactly _one_ DNS lookup, since some
531          * sites (i.e. ftp.us.debian.org) use round-robin DNS
532          * and we want to connect to only one IP... */
533         lsa = xhost2sockaddr(server.host, server.port);
534         if (!(opt & WGET_OPT_QUIET)) {
535                 fprintf(stderr, "Connecting to %s (%s)\n", server.host,
536                                 xmalloc_sockaddr2dotted(&lsa->u.sa));
537                 /* We leak result of xmalloc_sockaddr2dotted */
538         }
539
540         if (use_proxy || !target.is_ftp) {
541                 /*
542                  *  HTTP session
543                  */
544                 do {
545                         got_clen = 0;
546                         chunked = 0;
547
548                         if (!--try)
549                                 bb_error_msg_and_die("too many redirections");
550
551                         /* Open socket to http server */
552                         if (sfp) fclose(sfp);
553                         sfp = open_socket(lsa);
554
555                         /* Send HTTP request.  */
556                         if (use_proxy) {
557                                 fprintf(sfp, "GET %stp://%s/%s HTTP/1.1\r\n",
558                                         target.is_ftp ? "f" : "ht", target.host,
559                                         target.path);
560                         } else {
561                                 fprintf(sfp, "GET /%s HTTP/1.1\r\n", target.path);
562                         }
563
564                         fprintf(sfp, "Host: %s\r\nUser-Agent: %s\r\n",
565                                 target.host, user_agent);
566
567 #if ENABLE_FEATURE_WGET_AUTHENTICATION
568                         if (target.user) {
569                                 fprintf(sfp, "Proxy-Authorization: Basic %s\r\n"+6,
570                                         base64enc_512(buf, target.user));
571                         }
572                         if (use_proxy && server.user) {
573                                 fprintf(sfp, "Proxy-Authorization: Basic %s\r\n",
574                                         base64enc_512(buf, server.user));
575                         }
576 #endif
577
578                         if (beg_range)
579                                 fprintf(sfp, "Range: bytes=%"OFF_FMT"d-\r\n", beg_range);
580 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
581                         if (extra_headers)
582                                 fputs(extra_headers, sfp);
583 #endif
584                         fprintf(sfp, "Connection: close\r\n\r\n");
585
586                         /*
587                         * Retrieve HTTP response line and check for "200" status code.
588                         */
589  read_response:
590                         if (fgets(buf, sizeof(buf), sfp) == NULL)
591                                 bb_error_msg_and_die("no response from server");
592
593                         str = buf;
594                         str = skip_non_whitespace(str);
595                         str = skip_whitespace(str);
596                         // FIXME: no error check
597                         // xatou wouldn't work: "200 OK"
598                         status = atoi(str);
599                         switch (status) {
600                         case 0:
601                         case 100:
602                                 while (gethdr(buf, sizeof(buf), sfp /*, &n*/) != NULL)
603                                         /* eat all remaining headers */;
604                                 goto read_response;
605                         case 200:
606 /*
607 Response 204 doesn't say "null file", it says "metadata
608 has changed but data didn't":
609
610 "10.2.5 204 No Content
611 The server has fulfilled the request but does not need to return
612 an entity-body, and might want to return updated metainformation.
613 The response MAY include new or updated metainformation in the form
614 of entity-headers, which if present SHOULD be associated with
615 the requested variant.
616
617 If the client is a user agent, it SHOULD NOT change its document
618 view from that which caused the request to be sent. This response
619 is primarily intended to allow input for actions to take place
620 without causing a change to the user agent's active document view,
621 although any new or updated metainformation SHOULD be applied
622 to the document currently in the user agent's active view.
623
624 The 204 response MUST NOT include a message-body, and thus
625 is always terminated by the first empty line after the header fields."
626
627 However, in real world it was observed that some web servers
628 (e.g. Boa/0.94.14rc21) simply use code 204 when file size is zero.
629 */
630                         case 204:
631                                 break;
632                         case 300:       /* redirection */
633                         case 301:
634                         case 302:
635                         case 303:
636                                 break;
637                         case 206:
638                                 if (beg_range)
639                                         break;
640                                 /* fall through */
641                         default:
642                                 /* Show first line only and kill any ESC tricks */
643                                 buf[strcspn(buf, "\n\r\x1b")] = '\0';
644                                 bb_error_msg_and_die("server returned error: %s", buf);
645                         }
646
647                         /*
648                          * Retrieve HTTP headers.
649                          */
650                         while ((str = gethdr(buf, sizeof(buf), sfp /*, &n*/)) != NULL) {
651                                 /* gethdr did already convert the "FOO:" string to lowercase */
652                                 smalluint key = index_in_strings(keywords, *&buf) + 1;
653                                 if (key == KEY_content_length) {
654                                         content_len = BB_STRTOOFF(str, NULL, 10);
655                                         if (errno || content_len < 0) {
656                                                 bb_error_msg_and_die("content-length %s is garbage", str);
657                                         }
658                                         got_clen = 1;
659                                         continue;
660                                 }
661                                 if (key == KEY_transfer_encoding) {
662                                         if (index_in_strings(keywords, str_tolower(str)) + 1 != KEY_chunked)
663                                                 bb_error_msg_and_die("transfer encoding '%s' is not supported", str);
664                                         chunked = got_clen = 1;
665                                 }
666                                 if (key == KEY_location) {
667                                         if (str[0] == '/')
668                                                 /* free(target.allocated); */
669                                                 target.path = /* target.allocated = */ xstrdup(str+1);
670                                         else {
671                                                 parse_url(str, &target);
672                                                 if (use_proxy == 0) {
673                                                         server.host = target.host;
674                                                         server.port = target.port;
675                                                 }
676                                                 free(lsa);
677                                                 lsa = xhost2sockaddr(server.host, server.port);
678                                                 break;
679                                         }
680                                 }
681                         }
682                 } while (status >= 300);
683
684                 dfp = sfp;
685
686         } else {
687
688                 /*
689                  *  FTP session
690                  */
691                 if (!target.user)
692                         target.user = xstrdup("anonymous:busybox@");
693
694                 sfp = open_socket(lsa);
695                 if (ftpcmd(NULL, NULL, sfp, buf) != 220)
696                         bb_error_msg_and_die("%s", buf+4);
697
698                 /*
699                  * Splitting username:password pair,
700                  * trying to log in
701                  */
702                 str = strchr(target.user, ':');
703                 if (str)
704                         *(str++) = '\0';
705                 switch (ftpcmd("USER ", target.user, sfp, buf)) {
706                 case 230:
707                         break;
708                 case 331:
709                         if (ftpcmd("PASS ", str, sfp, buf) == 230)
710                                 break;
711                         /* fall through (failed login) */
712                 default:
713                         bb_error_msg_and_die("ftp login: %s", buf+4);
714                 }
715
716                 ftpcmd("TYPE I", NULL, sfp, buf);
717
718                 /*
719                  * Querying file size
720                  */
721                 if (ftpcmd("SIZE ", target.path, sfp, buf) == 213) {
722                         content_len = BB_STRTOOFF(buf+4, NULL, 10);
723                         if (errno || content_len < 0) {
724                                 bb_error_msg_and_die("SIZE value is garbage");
725                         }
726                         got_clen = 1;
727                 }
728
729                 /*
730                  * Entering passive mode
731                  */
732                 if (ftpcmd("PASV", NULL, sfp, buf) != 227) {
733  pasv_error:
734                         bb_error_msg_and_die("bad response to %s: %s", "PASV", buf);
735                 }
736                 // Response is "227 garbageN1,N2,N3,N4,P1,P2[)garbage]
737                 // Server's IP is N1.N2.N3.N4 (we ignore it)
738                 // Server's port for data connection is P1*256+P2
739                 str = strrchr(buf, ')');
740                 if (str) str[0] = '\0';
741                 str = strrchr(buf, ',');
742                 if (!str) goto pasv_error;
743                 port = xatou_range(str+1, 0, 255);
744                 *str = '\0';
745                 str = strrchr(buf, ',');
746                 if (!str) goto pasv_error;
747                 port += xatou_range(str+1, 0, 255) * 256;
748                 set_nport(lsa, htons(port));
749                 dfp = open_socket(lsa);
750
751                 if (beg_range) {
752                         sprintf(buf, "REST %"OFF_FMT"d", beg_range);
753                         if (ftpcmd(buf, NULL, sfp, buf) == 350)
754                                 content_len -= beg_range;
755                 }
756
757                 if (ftpcmd("RETR ", target.path, sfp, buf) > 150)
758                         bb_error_msg_and_die("bad response to %s: %s", "RETR", buf);
759         }
760
761         if (opt & WGET_OPT_SPIDER) {
762                 if (ENABLE_FEATURE_CLEAN_UP)
763                         fclose(sfp);
764                 return EXIT_SUCCESS;
765         }
766
767         /*
768          * Retrieve file
769          */
770
771         /* Do it before progress_meter (want to have nice error message) */
772         if (output_fd < 0) {
773                 int o_flags = O_WRONLY | O_CREAT | O_TRUNC | O_EXCL;
774                 /* compat with wget: -O FILE can overwrite */
775                 if (opt & WGET_OPT_OUTNAME)
776                         o_flags = O_WRONLY | O_CREAT | O_TRUNC;
777                 output_fd = xopen(fname_out, o_flags);
778         }
779
780         if (!(opt & WGET_OPT_QUIET))
781                 progress_meter(-1);
782
783         if (chunked)
784                 goto get_clen;
785
786         /* Loops only if chunked */
787         while (1) {
788                 while (content_len > 0 || !got_clen) {
789                         int n;
790                         unsigned rdsz = sizeof(buf);
791
792                         if (content_len < sizeof(buf) && (chunked || got_clen))
793                                 rdsz = (unsigned)content_len;
794                         n = safe_fread(buf, rdsz, dfp);
795                         if (n <= 0) {
796                                 if (ferror(dfp)) {
797                                         /* perror will not work: ferror doesn't set errno */
798                                         bb_error_msg_and_die(bb_msg_read_error);
799                                 }
800                                 break;
801                         }
802                         xwrite(output_fd, buf, n);
803 #if ENABLE_FEATURE_WGET_STATUSBAR
804                         transferred += n;
805 #endif
806                         if (got_clen)
807                                 content_len -= n;
808                 }
809
810                 if (!chunked)
811                         break;
812
813                 safe_fgets(buf, sizeof(buf), dfp); /* This is a newline */
814  get_clen:
815                 safe_fgets(buf, sizeof(buf), dfp);
816                 content_len = STRTOOFF(buf, NULL, 16);
817                 /* FIXME: error check? */
818                 if (content_len == 0)
819                         break; /* all done! */
820         }
821
822         if (!(opt & WGET_OPT_QUIET))
823                 progress_meter(0);
824
825         if ((use_proxy == 0) && target.is_ftp) {
826                 fclose(dfp);
827                 if (ftpcmd(NULL, NULL, sfp, buf) != 226)
828                         bb_error_msg_and_die("ftp error: %s", buf+4);
829                 ftpcmd("QUIT", NULL, sfp, buf);
830         }
831
832         return EXIT_SUCCESS;
833 }