f2d7daf2f886ce99bb472710c3ac5b5029f97d85
[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  * Licensed under GPLv2, see file LICENSE in this source tree.
7  *
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.
10  */
11 #include "libbb.h"
12
13 //#define log_io(...) bb_error_msg(__VA_ARGS__)
14 #define log_io(...) ((void)0)
15
16
17 struct host_info {
18         // May be used if we ever will want to free() all xstrdup()s...
19         /* char *allocated; */
20         const char *path;
21         const char *user;
22         char       *host;
23         int         port;
24         smallint    is_ftp;
25 };
26
27
28 /* Globals */
29 struct globals {
30         off_t content_len;        /* Content-length of the file */
31         off_t beg_range;          /* Range at which continue begins */
32 #if ENABLE_FEATURE_WGET_STATUSBAR
33         off_t transferred;        /* Number of bytes transferred so far */
34         const char *curfile;      /* Name of current file being transferred */
35         bb_progress_t pmt;
36 #endif
37 #if ENABLE_FEATURE_WGET_TIMEOUT
38         unsigned timeout_seconds;
39 #endif
40         smallint chunked;         /* chunked transfer encoding */
41         smallint got_clen;        /* got content-length: from server  */
42         /* Local downloads do benefit from big buffer.
43          * With 512 byte buffer, it was measured to be
44          * an order of magnitude slower than with big one.
45          */
46         uint64_t just_to_align_next_member;
47         char wget_buf[CONFIG_FEATURE_COPYBUF_KB*1024];
48 } FIX_ALIASING;
49 #define G (*ptr_to_globals)
50 #define INIT_G() do { \
51         SET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \
52         IF_FEATURE_WGET_TIMEOUT(G.timeout_seconds = 900;) \
53 } while (0)
54
55
56 /* Must match option string! */
57 enum {
58         WGET_OPT_CONTINUE   = (1 << 0),
59         WGET_OPT_SPIDER     = (1 << 1),
60         WGET_OPT_QUIET      = (1 << 2),
61         WGET_OPT_OUTNAME    = (1 << 3),
62         WGET_OPT_PREFIX     = (1 << 4),
63         WGET_OPT_PROXY      = (1 << 5),
64         WGET_OPT_USER_AGENT = (1 << 6),
65         WGET_OPT_NETWORK_READ_TIMEOUT = (1 << 7),
66         WGET_OPT_RETRIES    = (1 << 8),
67         WGET_OPT_PASSIVE    = (1 << 9),
68         WGET_OPT_HEADER     = (1 << 10) * ENABLE_FEATURE_WGET_LONG_OPTIONS,
69         WGET_OPT_POST_DATA  = (1 << 11) * ENABLE_FEATURE_WGET_LONG_OPTIONS,
70 };
71
72 enum {
73         PROGRESS_START = -1,
74         PROGRESS_END   = 0,
75         PROGRESS_BUMP  = 1,
76 };
77 #if ENABLE_FEATURE_WGET_STATUSBAR
78 static void progress_meter(int flag)
79 {
80         if (option_mask32 & WGET_OPT_QUIET)
81                 return;
82
83         if (flag == PROGRESS_START)
84                 bb_progress_init(&G.pmt, G.curfile);
85
86         bb_progress_update(&G.pmt, G.beg_range, G.transferred,
87                            G.chunked ? 0 : G.beg_range + G.transferred + G.content_len);
88
89         if (flag == PROGRESS_END) {
90                 bb_putchar_stderr('\n');
91                 G.transferred = 0;
92         }
93 }
94 #else
95 static ALWAYS_INLINE void progress_meter(int flag UNUSED_PARAM) { }
96 #endif
97
98
99 /* IPv6 knows scoped address types i.e. link and site local addresses. Link
100  * local addresses can have a scope identifier to specify the
101  * interface/link an address is valid on (e.g. fe80::1%eth0). This scope
102  * identifier is only valid on a single node.
103  *
104  * RFC 4007 says that the scope identifier MUST NOT be sent across the wire,
105  * unless all nodes agree on the semantic. Apache e.g. regards zone identifiers
106  * in the Host header as invalid requests, see
107  * https://issues.apache.org/bugzilla/show_bug.cgi?id=35122
108  */
109 static void strip_ipv6_scope_id(char *host)
110 {
111         char *scope, *cp;
112
113         /* bbox wget actually handles IPv6 addresses without [], like
114          * wget "http://::1/xxx", but this is not standard.
115          * To save code, _here_ we do not support it. */
116
117         if (host[0] != '[')
118                 return; /* not IPv6 */
119
120         scope = strchr(host, '%');
121         if (!scope)
122                 return;
123
124         /* Remove the IPv6 zone identifier from the host address */
125         cp = strchr(host, ']');
126         if (!cp || (cp[1] != ':' && cp[1] != '\0')) {
127                 /* malformed address (not "[xx]:nn" or "[xx]") */
128                 return;
129         }
130
131         /* cp points to "]...", scope points to "%eth0]..." */
132         overlapping_strcpy(scope, cp);
133 }
134
135 #if 0 /* were needed when we used signal-driven progress bar */
136 /* Read NMEMB bytes into PTR from STREAM.  Returns the number of bytes read,
137  * and a short count if an eof or non-interrupt error is encountered.  */
138 static size_t safe_fread(void *ptr, size_t nmemb, FILE *stream)
139 {
140         size_t ret;
141         char *p = (char*)ptr;
142
143         do {
144                 clearerr(stream);
145                 errno = 0;
146                 ret = fread(p, 1, nmemb, stream);
147                 p += ret;
148                 nmemb -= ret;
149         } while (nmemb && ferror(stream) && errno == EINTR);
150
151         return p - (char*)ptr;
152 }
153
154 /* Read a line or SIZE-1 bytes into S, whichever is less, from STREAM.
155  * Returns S, or NULL if an eof or non-interrupt error is encountered.  */
156 static char *safe_fgets(char *s, int size, FILE *stream)
157 {
158         char *ret;
159
160         do {
161                 clearerr(stream);
162                 errno = 0;
163                 ret = fgets(s, size, stream);
164         } while (ret == NULL && ferror(stream) && errno == EINTR);
165
166         return ret;
167 }
168 #endif
169
170 #if ENABLE_FEATURE_WGET_AUTHENTICATION
171 /* Base64-encode character string. */
172 static char *base64enc(const char *str)
173 {
174         unsigned len = strlen(str);
175         if (len > sizeof(G.wget_buf)/4*3 - 10) /* paranoia */
176                 len = sizeof(G.wget_buf)/4*3 - 10;
177         bb_uuencode(G.wget_buf, str, len, bb_uuenc_tbl_base64);
178         return G.wget_buf;
179 }
180 #endif
181
182 static char* sanitize_string(char *s)
183 {
184         unsigned char *p = (void *) s;
185         while (*p >= ' ')
186                 p++;
187         *p = '\0';
188         return s;
189 }
190
191 static FILE *open_socket(len_and_sockaddr *lsa)
192 {
193         FILE *fp;
194
195         /* glibc 2.4 seems to try seeking on it - ??! */
196         /* hopefully it understands what ESPIPE means... */
197         fp = fdopen(xconnect_stream(lsa), "r+");
198         if (fp == NULL)
199                 bb_perror_msg_and_die(bb_msg_memory_exhausted);
200
201         return fp;
202 }
203
204 /* Returns '\n' if it was seen, else '\0'. Trims at first '\r' or '\n' */
205 static char fgets_and_trim(FILE *fp)
206 {
207         char c;
208         char *buf_ptr;
209
210         if (fgets(G.wget_buf, sizeof(G.wget_buf) - 1, fp) == NULL)
211                 bb_perror_msg_and_die("error getting response");
212
213         buf_ptr = strchrnul(G.wget_buf, '\n');
214         c = *buf_ptr;
215         *buf_ptr = '\0';
216         buf_ptr = strchrnul(G.wget_buf, '\r');
217         *buf_ptr = '\0';
218
219         log_io("< %s", G.wget_buf);
220
221         return c;
222 }
223
224 static int ftpcmd(const char *s1, const char *s2, FILE *fp)
225 {
226         int result;
227         if (s1) {
228                 if (!s2)
229                         s2 = "";
230                 fprintf(fp, "%s%s\r\n", s1, s2);
231                 fflush(fp);
232                 log_io("> %s%s", s1, s2);
233         }
234
235         do {
236                 fgets_and_trim(fp);
237         } while (!isdigit(G.wget_buf[0]) || G.wget_buf[3] != ' ');
238
239         G.wget_buf[3] = '\0';
240         result = xatoi_positive(G.wget_buf);
241         G.wget_buf[3] = ' ';
242         return result;
243 }
244
245 static void parse_url(char *src_url, struct host_info *h)
246 {
247         char *url, *p, *sp;
248
249         /* h->allocated = */ url = xstrdup(src_url);
250
251         if (strncmp(url, "http://", 7) == 0) {
252                 h->port = bb_lookup_port("http", "tcp", 80);
253                 h->host = url + 7;
254                 h->is_ftp = 0;
255         } else if (strncmp(url, "ftp://", 6) == 0) {
256                 h->port = bb_lookup_port("ftp", "tcp", 21);
257                 h->host = url + 6;
258                 h->is_ftp = 1;
259         } else
260                 bb_error_msg_and_die("not an http or ftp url: %s", sanitize_string(url));
261
262         // FYI:
263         // "Real" wget 'http://busybox.net?var=a/b' sends this request:
264         //   'GET /?var=a/b HTTP 1.0'
265         //   and saves 'index.html?var=a%2Fb' (we save 'b')
266         // wget 'http://busybox.net?login=john@doe':
267         //   request: 'GET /?login=john@doe HTTP/1.0'
268         //   saves: 'index.html?login=john@doe' (we save '?login=john@doe')
269         // wget 'http://busybox.net#test/test':
270         //   request: 'GET / HTTP/1.0'
271         //   saves: 'index.html' (we save 'test')
272         //
273         // We also don't add unique .N suffix if file exists...
274         sp = strchr(h->host, '/');
275         p = strchr(h->host, '?'); if (!sp || (p && sp > p)) sp = p;
276         p = strchr(h->host, '#'); if (!sp || (p && sp > p)) sp = p;
277         if (!sp) {
278                 h->path = "";
279         } else if (*sp == '/') {
280                 *sp = '\0';
281                 h->path = sp + 1;
282         } else { // '#' or '?'
283                 // http://busybox.net?login=john@doe is a valid URL
284                 // memmove converts to:
285                 // http:/busybox.nett?login=john@doe...
286                 memmove(h->host - 1, h->host, sp - h->host);
287                 h->host--;
288                 sp[-1] = '\0';
289                 h->path = sp;
290         }
291
292         // We used to set h->user to NULL here, but this interferes
293         // with handling of code 302 ("object was moved")
294
295         sp = strrchr(h->host, '@');
296         if (sp != NULL) {
297                 h->user = h->host;
298                 *sp = '\0';
299                 h->host = sp + 1;
300         }
301
302         sp = h->host;
303 }
304
305 static char *gethdr(FILE *fp)
306 {
307         char *s, *hdrval;
308         int c;
309
310         /* *istrunc = 0; */
311
312         /* retrieve header line */
313         c = fgets_and_trim(fp);
314
315         /* end of the headers? */
316         if (G.wget_buf[0] == '\0')
317                 return NULL;
318
319         /* convert the header name to lower case */
320         for (s = G.wget_buf; isalnum(*s) || *s == '-' || *s == '.'; ++s) {
321                 /* tolower for "A-Z", no-op for "0-9a-z-." */
322                 *s |= 0x20;
323         }
324
325         /* verify we are at the end of the header name */
326         if (*s != ':')
327                 bb_error_msg_and_die("bad header line: %s", sanitize_string(G.wget_buf));
328
329         /* locate the start of the header value */
330         *s++ = '\0';
331         hdrval = skip_whitespace(s);
332
333         if (c != '\n') {
334                 /* Rats! The buffer isn't big enough to hold the entire header value */
335                 while (c = getc(fp), c != EOF && c != '\n')
336                         continue;
337         }
338
339         return hdrval;
340 }
341
342 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
343 static char *URL_escape(const char *str)
344 {
345         /* URL encode, see RFC 2396 */
346         char *dst;
347         char *res = dst = xmalloc(strlen(str) * 3 + 1);
348         unsigned char c;
349
350         while (1) {
351                 c = *str++;
352                 if (c == '\0'
353                 /* || strchr("!&'()*-.=_~", c) - more code */
354                  || c == '!'
355                  || c == '&'
356                  || c == '\''
357                  || c == '('
358                  || c == ')'
359                  || c == '*'
360                  || c == '-'
361                  || c == '.'
362                  || c == '='
363                  || c == '_'
364                  || c == '~'
365                  || (c >= '0' && c <= '9')
366                  || ((c|0x20) >= 'a' && (c|0x20) <= 'z')
367                 ) {
368                         *dst++ = c;
369                         if (c == '\0')
370                                 return res;
371                 } else {
372                         *dst++ = '%';
373                         *dst++ = bb_hexdigits_upcase[c >> 4];
374                         *dst++ = bb_hexdigits_upcase[c & 0xf];
375                 }
376         }
377 }
378 #endif
379
380 static FILE* prepare_ftp_session(FILE **dfpp, struct host_info *target, len_and_sockaddr *lsa)
381 {
382         FILE *sfp;
383         char *str;
384         int port;
385
386         if (!target->user)
387                 target->user = xstrdup("anonymous:busybox@");
388
389         sfp = open_socket(lsa);
390         if (ftpcmd(NULL, NULL, sfp) != 220)
391                 bb_error_msg_and_die("%s", sanitize_string(G.wget_buf + 4));
392
393         /*
394          * Splitting username:password pair,
395          * trying to log in
396          */
397         str = strchr(target->user, ':');
398         if (str)
399                 *str++ = '\0';
400         switch (ftpcmd("USER ", target->user, sfp)) {
401         case 230:
402                 break;
403         case 331:
404                 if (ftpcmd("PASS ", str, sfp) == 230)
405                         break;
406                 /* fall through (failed login) */
407         default:
408                 bb_error_msg_and_die("ftp login: %s", sanitize_string(G.wget_buf + 4));
409         }
410
411         ftpcmd("TYPE I", NULL, sfp);
412
413         /*
414          * Querying file size
415          */
416         if (ftpcmd("SIZE ", target->path, sfp) == 213) {
417                 G.content_len = BB_STRTOOFF(G.wget_buf + 4, NULL, 10);
418                 if (G.content_len < 0 || errno) {
419                         bb_error_msg_and_die("SIZE value is garbage");
420                 }
421                 G.got_clen = 1;
422         }
423
424         /*
425          * Entering passive mode
426          */
427         if (ftpcmd("PASV", NULL, sfp) != 227) {
428  pasv_error:
429                 bb_error_msg_and_die("bad response to %s: %s", "PASV", sanitize_string(G.wget_buf));
430         }
431         // Response is "227 garbageN1,N2,N3,N4,P1,P2[)garbage]
432         // Server's IP is N1.N2.N3.N4 (we ignore it)
433         // Server's port for data connection is P1*256+P2
434         str = strrchr(G.wget_buf, ')');
435         if (str) str[0] = '\0';
436         str = strrchr(G.wget_buf, ',');
437         if (!str) goto pasv_error;
438         port = xatou_range(str+1, 0, 255);
439         *str = '\0';
440         str = strrchr(G.wget_buf, ',');
441         if (!str) goto pasv_error;
442         port += xatou_range(str+1, 0, 255) * 256;
443         set_nport(lsa, htons(port));
444
445         *dfpp = open_socket(lsa);
446
447         if (G.beg_range) {
448                 sprintf(G.wget_buf, "REST %"OFF_FMT"u", G.beg_range);
449                 if (ftpcmd(G.wget_buf, NULL, sfp) == 350)
450                         G.content_len -= G.beg_range;
451         }
452
453         if (ftpcmd("RETR ", target->path, sfp) > 150)
454                 bb_error_msg_and_die("bad response to %s: %s", "RETR", sanitize_string(G.wget_buf));
455
456         return sfp;
457 }
458
459 static void NOINLINE retrieve_file_data(FILE *dfp, int output_fd)
460 {
461 #if ENABLE_FEATURE_WGET_STATUSBAR || ENABLE_FEATURE_WGET_TIMEOUT
462 # if ENABLE_FEATURE_WGET_TIMEOUT
463         unsigned second_cnt;
464 # endif
465         struct pollfd polldata;
466
467         polldata.fd = fileno(dfp);
468         polldata.events = POLLIN | POLLPRI;
469         ndelay_on(polldata.fd);
470 #endif
471         progress_meter(PROGRESS_START);
472
473         if (G.chunked)
474                 goto get_clen;
475
476         /* Loops only if chunked */
477         while (1) {
478                 while (1) {
479                         int n;
480                         unsigned rdsz;
481
482                         rdsz = sizeof(G.wget_buf);
483                         if (G.got_clen) {
484                                 if (G.content_len < (off_t)sizeof(G.wget_buf)) {
485                                         if ((int)G.content_len <= 0)
486                                                 break;
487                                         rdsz = (unsigned)G.content_len;
488                                 }
489                         }
490 #if ENABLE_FEATURE_WGET_STATUSBAR || ENABLE_FEATURE_WGET_TIMEOUT
491 # if ENABLE_FEATURE_WGET_TIMEOUT
492                         second_cnt = G.timeout_seconds;
493 # endif
494                         while (1) {
495                                 if (safe_poll(&polldata, 1, 1000) != 0)
496                                         break; /* error, EOF, or data is available */
497 # if ENABLE_FEATURE_WGET_TIMEOUT
498                                 if (second_cnt != 0 && --second_cnt == 0) {
499                                         progress_meter(PROGRESS_END);
500                                         bb_perror_msg_and_die("download timed out");
501                                 }
502 # endif
503                                 /* Needed for "stalled" indicator */
504                                 progress_meter(PROGRESS_BUMP);
505                         }
506 #endif
507                         n = fread(G.wget_buf, 1, rdsz, dfp);
508                         if (n <= 0) {
509                                 if (ferror(dfp)) {
510                                         /* perror will not work: ferror doesn't set errno */
511                                         bb_error_msg_and_die(bb_msg_read_error);
512                                 }
513                                 break;
514                         }
515                         xwrite(output_fd, G.wget_buf, n);
516 #if ENABLE_FEATURE_WGET_STATUSBAR
517                         G.transferred += n;
518                         progress_meter(PROGRESS_BUMP);
519 #endif
520                         if (G.got_clen) {
521                                 G.content_len -= n;
522                                 if (G.content_len == 0)
523                                         break;
524                         }
525                 }
526
527                 if (!G.chunked)
528                         break;
529
530                 fgets_and_trim(dfp); /* This is a newline */
531  get_clen:
532                 fgets_and_trim(dfp);
533                 G.content_len = STRTOOFF(G.wget_buf, NULL, 16);
534                 /* FIXME: error check? */
535                 if (G.content_len == 0)
536                         break; /* all done! */
537                 G.got_clen = 1;
538         }
539
540         progress_meter(PROGRESS_END);
541 }
542
543 int wget_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
544 int wget_main(int argc UNUSED_PARAM, char **argv)
545 {
546         struct host_info server, target;
547         len_and_sockaddr *lsa;
548         unsigned opt;
549         int redir_limit;
550         char *proxy = NULL;
551         char *dir_prefix = NULL;
552 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
553         char *post_data;
554         char *extra_headers = NULL;
555         llist_t *headers_llist = NULL;
556 #endif
557         FILE *sfp;                      /* socket to web/ftp server         */
558         FILE *dfp;                      /* socket to ftp server (data)      */
559         char *fname_out;                /* where to direct output (-O)      */
560         int output_fd = -1;
561         bool use_proxy;                 /* Use proxies if env vars are set  */
562         const char *proxy_flag = "on";  /* Use proxies if env vars are set  */
563         const char *user_agent = "Wget";/* "User-Agent" header field        */
564
565         static const char keywords[] ALIGN1 =
566                 "content-length\0""transfer-encoding\0""chunked\0""location\0";
567         enum {
568                 KEY_content_length = 1, KEY_transfer_encoding, KEY_chunked, KEY_location
569         };
570 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
571         static const char wget_longopts[] ALIGN1 =
572                 /* name, has_arg, val */
573                 "continue\0"         No_argument       "c"
574                 "spider\0"           No_argument       "s"
575                 "quiet\0"            No_argument       "q"
576                 "output-document\0"  Required_argument "O"
577                 "directory-prefix\0" Required_argument "P"
578                 "proxy\0"            Required_argument "Y"
579                 "user-agent\0"       Required_argument "U"
580 #if ENABLE_FEATURE_WGET_TIMEOUT
581                 "timeout\0"          Required_argument "T"
582 #endif
583                 /* Ignored: */
584                 // "tries\0"            Required_argument "t"
585                 /* Ignored (we always use PASV): */
586                 "passive-ftp\0"      No_argument       "\xff"
587                 "header\0"           Required_argument "\xfe"
588                 "post-data\0"        Required_argument "\xfd"
589                 /* Ignored (we don't do ssl) */
590                 "no-check-certificate\0" No_argument   "\xfc"
591                 ;
592 #endif
593
594         INIT_G();
595
596 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
597         applet_long_options = wget_longopts;
598 #endif
599         /* server.allocated = target.allocated = NULL; */
600         opt_complementary = "-1" IF_FEATURE_WGET_TIMEOUT(":T+") IF_FEATURE_WGET_LONG_OPTIONS(":\xfe::");
601         opt = getopt32(argv, "csqO:P:Y:U:T:" /*ignored:*/ "t:",
602                                 &fname_out, &dir_prefix,
603                                 &proxy_flag, &user_agent,
604                                 IF_FEATURE_WGET_TIMEOUT(&G.timeout_seconds) IF_NOT_FEATURE_WGET_TIMEOUT(NULL),
605                                 NULL /* -t RETRIES */
606                                 IF_FEATURE_WGET_LONG_OPTIONS(, &headers_llist)
607                                 IF_FEATURE_WGET_LONG_OPTIONS(, &post_data)
608                                 );
609 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
610         if (headers_llist) {
611                 int size = 1;
612                 char *cp;
613                 llist_t *ll = headers_llist;
614                 while (ll) {
615                         size += strlen(ll->data) + 2;
616                         ll = ll->link;
617                 }
618                 extra_headers = cp = xmalloc(size);
619                 while (headers_llist) {
620                         cp += sprintf(cp, "%s\r\n", (char*)llist_pop(&headers_llist));
621                 }
622         }
623 #endif
624
625         /* TODO: compat issue: should handle "wget URL1 URL2..." */
626
627         target.user = NULL;
628         parse_url(argv[optind], &target);
629
630         /* Use the proxy if necessary */
631         use_proxy = (strcmp(proxy_flag, "off") != 0);
632         if (use_proxy) {
633                 proxy = getenv(target.is_ftp ? "ftp_proxy" : "http_proxy");
634                 if (proxy && proxy[0]) {
635                         server.user = NULL;
636                         parse_url(proxy, &server);
637                 } else {
638                         use_proxy = 0;
639                 }
640         }
641         if (!use_proxy) {
642                 server.port = target.port;
643                 if (ENABLE_FEATURE_IPV6) {
644                         server.host = xstrdup(target.host);
645                 } else {
646                         server.host = target.host;
647                 }
648         }
649
650         if (ENABLE_FEATURE_IPV6)
651                 strip_ipv6_scope_id(target.host);
652
653         /* Guess an output filename, if there was no -O FILE */
654         if (!(opt & WGET_OPT_OUTNAME)) {
655                 fname_out = bb_get_last_path_component_nostrip(target.path);
656                 /* handle "wget http://kernel.org//" */
657                 if (fname_out[0] == '/' || !fname_out[0])
658                         fname_out = (char*)"index.html";
659                 /* -P DIR is considered only if there was no -O FILE */
660                 if (dir_prefix)
661                         fname_out = concat_path_file(dir_prefix, fname_out);
662         } else {
663                 if (LONE_DASH(fname_out)) {
664                         /* -O - */
665                         output_fd = 1;
666                         opt &= ~WGET_OPT_CONTINUE;
667                 }
668         }
669 #if ENABLE_FEATURE_WGET_STATUSBAR
670         G.curfile = bb_get_last_path_component_nostrip(fname_out);
671 #endif
672
673         /* Impossible?
674         if ((opt & WGET_OPT_CONTINUE) && !fname_out)
675                 bb_error_msg_and_die("can't specify continue (-c) without a filename (-O)");
676         */
677
678         /* Determine where to start transfer */
679         if (opt & WGET_OPT_CONTINUE) {
680                 output_fd = open(fname_out, O_WRONLY);
681                 if (output_fd >= 0) {
682                         G.beg_range = xlseek(output_fd, 0, SEEK_END);
683                 }
684                 /* File doesn't exist. We do not create file here yet.
685                  * We are not sure it exists on remove side */
686         }
687
688         redir_limit = 5;
689  resolve_lsa:
690         lsa = xhost2sockaddr(server.host, server.port);
691         if (!(opt & WGET_OPT_QUIET)) {
692                 char *s = xmalloc_sockaddr2dotted(&lsa->u.sa);
693                 fprintf(stderr, "Connecting to %s (%s)\n", server.host, s);
694                 free(s);
695         }
696  establish_session:
697         if (use_proxy || !target.is_ftp) {
698                 /*
699                  *  HTTP session
700                  */
701                 char *str;
702                 int status;
703
704                 /* Open socket to http server */
705                 sfp = open_socket(lsa);
706
707                 /* Send HTTP request */
708                 if (use_proxy) {
709                         fprintf(sfp, "GET %stp://%s/%s HTTP/1.1\r\n",
710                                 target.is_ftp ? "f" : "ht", target.host,
711                                 target.path);
712                 } else {
713                         if (opt & WGET_OPT_POST_DATA)
714                                 fprintf(sfp, "POST /%s HTTP/1.1\r\n", target.path);
715                         else
716                                 fprintf(sfp, "GET /%s HTTP/1.1\r\n", target.path);
717                 }
718
719                 fprintf(sfp, "Host: %s\r\nUser-Agent: %s\r\n",
720                         target.host, user_agent);
721
722                 /* Ask server to close the connection as soon as we are done
723                  * (IOW: we do not intend to send more requests)
724                  */
725                 fprintf(sfp, "Connection: close\r\n");
726
727 #if ENABLE_FEATURE_WGET_AUTHENTICATION
728                 if (target.user) {
729                         fprintf(sfp, "Proxy-Authorization: Basic %s\r\n"+6,
730                                 base64enc(target.user));
731                 }
732                 if (use_proxy && server.user) {
733                         fprintf(sfp, "Proxy-Authorization: Basic %s\r\n",
734                                 base64enc(server.user));
735                 }
736 #endif
737
738                 if (G.beg_range)
739                         fprintf(sfp, "Range: bytes=%"OFF_FMT"u-\r\n", G.beg_range);
740
741 #if ENABLE_FEATURE_WGET_LONG_OPTIONS
742                 if (extra_headers)
743                         fputs(extra_headers, sfp);
744
745                 if (opt & WGET_OPT_POST_DATA) {
746                         char *estr = URL_escape(post_data);
747                         fprintf(sfp,
748                                 "Content-Type: application/x-www-form-urlencoded\r\n"
749                                 "Content-Length: %u\r\n"
750                                 "\r\n"
751                                 "%s",
752                                 (int) strlen(estr), estr
753                         );
754                         free(estr);
755                 } else
756 #endif
757                 {
758                         fprintf(sfp, "\r\n");
759                 }
760
761                 fflush(sfp);
762
763                 /*
764                  * Retrieve HTTP response line and check for "200" status code.
765                  */
766  read_response:
767                 fgets_and_trim(sfp);
768
769                 str = G.wget_buf;
770                 str = skip_non_whitespace(str);
771                 str = skip_whitespace(str);
772                 // FIXME: no error check
773                 // xatou wouldn't work: "200 OK"
774                 status = atoi(str);
775                 switch (status) {
776                 case 0:
777                 case 100:
778                         while (gethdr(sfp /*, &n*/) != NULL)
779                                 /* eat all remaining headers */;
780                         goto read_response;
781                 case 200:
782 /*
783 Response 204 doesn't say "null file", it says "metadata
784 has changed but data didn't":
785
786 "10.2.5 204 No Content
787 The server has fulfilled the request but does not need to return
788 an entity-body, and might want to return updated metainformation.
789 The response MAY include new or updated metainformation in the form
790 of entity-headers, which if present SHOULD be associated with
791 the requested variant.
792
793 If the client is a user agent, it SHOULD NOT change its document
794 view from that which caused the request to be sent. This response
795 is primarily intended to allow input for actions to take place
796 without causing a change to the user agent's active document view,
797 although any new or updated metainformation SHOULD be applied
798 to the document currently in the user agent's active view.
799
800 The 204 response MUST NOT include a message-body, and thus
801 is always terminated by the first empty line after the header fields."
802
803 However, in real world it was observed that some web servers
804 (e.g. Boa/0.94.14rc21) simply use code 204 when file size is zero.
805 */
806                 case 204:
807                         break;
808                 case 300:  /* redirection */
809                 case 301:
810                 case 302:
811                 case 303:
812                         break;
813                 case 206:
814                         if (G.beg_range)
815                                 break;
816                         /* fall through */
817                 default:
818                         bb_error_msg_and_die("server returned error: %s", sanitize_string(G.wget_buf));
819                 }
820
821                 /*
822                  * Retrieve HTTP headers.
823                  */
824                 while ((str = gethdr(sfp /*, &n*/)) != NULL) {
825                         /* gethdr converted "FOO:" string to lowercase */
826                         smalluint key;
827                         /* strip trailing whitespace */
828                         char *s = strchrnul(str, '\0') - 1;
829                         while (s >= str && (*s == ' ' || *s == '\t')) {
830                                 *s = '\0';
831                                 s--;
832                         }
833                         key = index_in_strings(keywords, G.wget_buf) + 1;
834                         if (key == KEY_content_length) {
835                                 G.content_len = BB_STRTOOFF(str, NULL, 10);
836                                 if (G.content_len < 0 || errno) {
837                                         bb_error_msg_and_die("content-length %s is garbage", sanitize_string(str));
838                                 }
839                                 G.got_clen = 1;
840                                 continue;
841                         }
842                         if (key == KEY_transfer_encoding) {
843                                 if (index_in_strings(keywords, str_tolower(str)) + 1 != KEY_chunked)
844                                         bb_error_msg_and_die("transfer encoding '%s' is not supported", sanitize_string(str));
845                                 G.chunked = G.got_clen = 1;
846                         }
847                         if (key == KEY_location && status >= 300) {
848                                 if (--redir_limit == 0)
849                                         bb_error_msg_and_die("too many redirections");
850                                 fclose(sfp);
851                                 G.got_clen = 0;
852                                 G.chunked = 0;
853                                 if (str[0] == '/')
854                                         /* free(target.allocated); */
855                                         target.path = /* target.allocated = */ xstrdup(str+1);
856                                         /* lsa stays the same: it's on the same server */
857                                 else {
858                                         parse_url(str, &target);
859                                         if (!use_proxy) {
860                                                 server.host = target.host;
861                                                 /* strip_ipv6_scope_id(target.host); - no! */
862                                                 /* we assume remote never gives us IPv6 addr with scope id */
863                                                 server.port = target.port;
864                                                 free(lsa);
865                                                 goto resolve_lsa;
866                                         } /* else: lsa stays the same: we use proxy */
867                                 }
868                                 goto establish_session;
869                         }
870                 }
871 //              if (status >= 300)
872 //                      bb_error_msg_and_die("bad redirection (no Location: header from server)");
873
874                 /* For HTTP, data is pumped over the same connection */
875                 dfp = sfp;
876
877         } else {
878                 /*
879                  *  FTP session
880                  */
881                 sfp = prepare_ftp_session(&dfp, &target, lsa);
882         }
883
884         if (opt & WGET_OPT_SPIDER) {
885                 if (ENABLE_FEATURE_CLEAN_UP)
886                         fclose(sfp);
887                 return EXIT_SUCCESS;
888         }
889
890         if (output_fd < 0) {
891                 int o_flags = O_WRONLY | O_CREAT | O_TRUNC | O_EXCL;
892                 /* compat with wget: -O FILE can overwrite */
893                 if (opt & WGET_OPT_OUTNAME)
894                         o_flags = O_WRONLY | O_CREAT | O_TRUNC;
895                 output_fd = xopen(fname_out, o_flags);
896         }
897
898         retrieve_file_data(dfp, output_fd);
899         xclose(output_fd);
900
901         if (dfp != sfp) {
902                 /* It's ftp. Close it properly */
903                 fclose(dfp);
904                 if (ftpcmd(NULL, NULL, sfp) != 226)
905                         bb_error_msg_and_die("ftp error: %s", sanitize_string(G.wget_buf + 4));
906                 /* ftpcmd("QUIT", NULL, sfp); - why bother? */
907         }
908
909         return EXIT_SUCCESS;
910 }