* Copyright (C) 2010 Bradley M. Kuhn <bkuhn@ebb.org>
* Kuhn's copyrights are licensed GPLv2-or-later. File as a whole remains GPLv2.
*/
+
+//usage:#define wget_trivial_usage
+//usage: IF_FEATURE_WGET_LONG_OPTIONS(
+//usage: "[-c|--continue] [-s|--spider] [-q|--quiet] [-O|--output-document FILE]\n"
+//usage: " [--header 'header: value'] [-Y|--proxy on/off] [-P DIR]\n"
+/* Since we ignore these opts, we don't show them in --help */
+/* //usage: " [--no-check-certificate] [--no-cache]" */
+//usage: " [-U|--user-agent AGENT]" IF_FEATURE_WGET_TIMEOUT(" [-T SEC]") " URL..."
+//usage: )
+//usage: IF_NOT_FEATURE_WGET_LONG_OPTIONS(
+//usage: "[-csq] [-O FILE] [-Y on/off] [-P DIR] [-U AGENT]"
+//usage: IF_FEATURE_WGET_TIMEOUT(" [-T SEC]") " URL..."
+//usage: )
+//usage:#define wget_full_usage "\n\n"
+//usage: "Retrieve files via HTTP or FTP\n"
+//usage: "\n -s Spider mode - only check file existence"
+//usage: "\n -c Continue retrieval of aborted transfer"
+//usage: "\n -q Quiet"
+//usage: "\n -P DIR Save to DIR (default .)"
+//usage: IF_FEATURE_WGET_TIMEOUT(
+//usage: "\n -T SEC Network read timeout is SEC seconds"
+//usage: )
+//usage: "\n -O FILE Save to FILE ('-' for stdout)"
+//usage: "\n -U STR Use STR for User-Agent header"
+//usage: "\n -Y Use proxy ('on' or 'off')"
+
#include "libbb.h"
-//#define log_io(...) bb_error_msg(__VA_ARGS__)
-#define log_io(...) ((void)0)
+#if 0
+# define log_io(...) bb_error_msg(__VA_ARGS__)
+#else
+# define log_io(...) ((void)0)
+#endif
struct host_info {
sp = strrchr(h->host, '@');
if (sp != NULL) {
- h->user = h->host;
+ // URL-decode "user:password" string before base64-encoding:
+ // wget http://test:my%20pass@example.com should send
+ // Authorization: Basic dGVzdDpteSBwYXNz
+ // which decodes to "test:my pass".
+ // Standard wget and curl do this too.
*sp = '\0';
+ h->user = percent_decode_in_place(h->host, /*strict:*/ 0);
h->host = sp + 1;
}
char *s, *hdrval;
int c;
- /* *istrunc = 0; */
-
/* retrieve header line */
c = fgets_and_trim(fp);
return NULL;
/* convert the header name to lower case */
- for (s = G.wget_buf; isalnum(*s) || *s == '-' || *s == '.'; ++s) {
- /* tolower for "A-Z", no-op for "0-9a-z-." */
+ for (s = G.wget_buf; isalnum(*s) || *s == '-' || *s == '.' || *s == '_'; ++s) {
+ /*
+ * No-op for 20-3f and 60-7f. "0-9a-z-." are in these ranges.
+ * 40-5f range ("@A-Z[\]^_") maps to 60-7f.
+ * "A-Z" maps to "a-z".
+ * "@[\]" can't occur in header names.
+ * "^_" maps to "~,DEL" (which is wrong).
+ * "^" was never seen yet, "_" was seen from web.archive.org
+ * (x-archive-orig-x_commoncrawl_Signature: HEXSTRING).
+ */
*s |= 0x20;
}
return hdrval;
}
-#if ENABLE_FEATURE_WGET_LONG_OPTIONS
-static char *URL_escape(const char *str)
+static void reset_beg_range_to_zero(void)
{
- /* URL encode, see RFC 2396 */
- char *dst;
- char *res = dst = xmalloc(strlen(str) * 3 + 1);
- unsigned char c;
-
- while (1) {
- c = *str++;
- if (c == '\0'
- /* || strchr("!&'()*-.=_~", c) - more code */
- || c == '!'
- || c == '&'
- || c == '\''
- || c == '('
- || c == ')'
- || c == '*'
- || c == '-'
- || c == '.'
- || c == '='
- || c == '_'
- || c == '~'
- || (c >= '0' && c <= '9')
- || ((c|0x20) >= 'a' && (c|0x20) <= 'z')
- ) {
- *dst++ = c;
- if (c == '\0')
- return res;
- } else {
- *dst++ = '%';
- *dst++ = bb_hexdigits_upcase[c >> 4];
- *dst++ = bb_hexdigits_upcase[c & 0xf];
- }
- }
+ bb_error_msg("restart failed");
+ G.beg_range = 0;
+ xlseek(G.output_fd, 0, SEEK_SET);
+ /* Done at the end instead: */
+ /* ftruncate(G.output_fd, 0); */
}
-#endif
static FILE* prepare_ftp_session(FILE **dfpp, struct host_info *target, len_and_sockaddr *lsa)
{
str = strrchr(G.wget_buf, ',');
if (!str) goto pasv_error;
port += xatou_range(str+1, 0, 255) * 256;
- set_nport(lsa, htons(port));
+ set_nport(&lsa->u.sa, htons(port));
*dfpp = open_socket(lsa);
- if (G.beg_range) {
+ if (G.beg_range != 0) {
sprintf(G.wget_buf, "REST %"OFF_FMT"u", G.beg_range);
if (ftpcmd(G.wget_buf, NULL, sfp) == 350)
G.content_len -= G.beg_range;
+ else
+ reset_beg_range_to_zero();
}
if (ftpcmd("RETR ", target->path, sfp) > 150)
{
#if ENABLE_FEATURE_WGET_STATUSBAR || ENABLE_FEATURE_WGET_TIMEOUT
# if ENABLE_FEATURE_WGET_TIMEOUT
- unsigned second_cnt;
+ unsigned second_cnt = G.timeout_seconds;
# endif
struct pollfd polldata;
* which messes up progress bar and/or timeout logic.
* Because of nonblocking I/O, we need to dance
* very carefully around EAGAIN. See explanation at
- * clearerr() call.
+ * clearerr() calls.
*/
ndelay_on(polldata.fd);
#endif
int n;
unsigned rdsz;
- rdsz = sizeof(G.wget_buf);
- if (G.got_clen) {
- if (G.content_len < (off_t)sizeof(G.wget_buf)) {
- if ((int)G.content_len <= 0)
- break;
- rdsz = (unsigned)G.content_len;
- }
- }
-
#if ENABLE_FEATURE_WGET_STATUSBAR || ENABLE_FEATURE_WGET_TIMEOUT
-# if ENABLE_FEATURE_WGET_TIMEOUT
- second_cnt = G.timeout_seconds;
-# endif
- while (1) {
- if (safe_poll(&polldata, 1, 1000) != 0)
- break; /* error, EOF, or data is available */
-# if ENABLE_FEATURE_WGET_TIMEOUT
- if (second_cnt != 0 && --second_cnt == 0) {
- progress_meter(PROGRESS_END);
- bb_error_msg_and_die("download timed out");
- }
-# endif
- /* Needed for "stalled" indicator */
- progress_meter(PROGRESS_BUMP);
- }
-
/* fread internally uses read loop, which in our case
* is usually exited when we get EAGAIN.
* In this case, libc sets error marker on the stream.
* into if (n <= 0) ...
*/
clearerr(dfp);
- errno = 0;
#endif
+ errno = 0;
+ rdsz = sizeof(G.wget_buf);
+ if (G.got_clen) {
+ if (G.content_len < (off_t)sizeof(G.wget_buf)) {
+ if ((int)G.content_len <= 0)
+ break;
+ rdsz = (unsigned)G.content_len;
+ }
+ }
n = fread(G.wget_buf, 1, rdsz, dfp);
- /* man fread:
+
+ if (n > 0) {
+ xwrite(G.output_fd, G.wget_buf, n);
+#if ENABLE_FEATURE_WGET_STATUSBAR
+ G.transferred += n;
+#endif
+ if (G.got_clen) {
+ G.content_len -= n;
+ if (G.content_len == 0)
+ break;
+ }
+#if ENABLE_FEATURE_WGET_TIMEOUT
+ second_cnt = G.timeout_seconds;
+#endif
+ continue;
+ }
+
+ /* n <= 0.
+ * man fread:
* If error occurs, or EOF is reached, the return value
* is a short item count (or zero).
* fread does not distinguish between EOF and error.
*/
- if (n <= 0) {
-#if ENABLE_FEATURE_WGET_STATUSBAR || ENABLE_FEATURE_WGET_TIMEOUT
- if (errno == EAGAIN) /* poll lied, there is no data? */
- continue; /* yes */
-#endif
- if (ferror(dfp))
+ if (errno != EAGAIN) {
+ if (ferror(dfp)) {
+ progress_meter(PROGRESS_END);
bb_perror_msg_and_die(bb_msg_read_error);
+ }
break; /* EOF, not error */
}
- xwrite(G.output_fd, G.wget_buf, n);
-
-#if ENABLE_FEATURE_WGET_STATUSBAR
- G.transferred += n;
+#if ENABLE_FEATURE_WGET_STATUSBAR || ENABLE_FEATURE_WGET_TIMEOUT
+ /* It was EAGAIN. There is no data. Wait up to one second
+ * then abort if timed out, or update the bar and try reading again.
+ */
+ if (safe_poll(&polldata, 1, 1000) == 0) {
+# if ENABLE_FEATURE_WGET_TIMEOUT
+ if (second_cnt != 0 && --second_cnt == 0) {
+ progress_meter(PROGRESS_END);
+ bb_error_msg_and_die("download timed out");
+ }
+# endif
+ /* We used to loop back to poll here,
+ * but there is no great harm in letting fread
+ * to try reading anyway.
+ */
+ }
+ /* Need to do it _every_ second for "stalled" indicator
+ * to be shown properly.
+ */
progress_meter(PROGRESS_BUMP);
#endif
- if (G.got_clen) {
- G.content_len -= n;
- if (G.content_len == 0)
- break;
- }
- }
+ } /* while (reading data) */
+
#if ENABLE_FEATURE_WGET_STATUSBAR || ENABLE_FEATURE_WGET_TIMEOUT
clearerr(dfp);
ndelay_off(polldata.fd); /* else fgets can get very unhappy */
if (G.content_len == 0)
break; /* all done! */
G.got_clen = 1;
+ /*
+ * Note that fgets may result in some data being buffered in dfp.
+ * We loop back to fread, which will retrieve this data.
+ * Also note that code has to be arranged so that fread
+ * is done _before_ one-second poll wait - poll doesn't know
+ * about stdio buffering and can result in spurious one second waits!
+ */
+ }
+
+ /* If -c failed, we restart from the beginning,
+ * but we do not truncate file then, we do it only now, at the end.
+ * This lets user to ^C if his 99% complete 10 GB file download
+ * failed to restart *without* losing the almost complete file.
+ */
+ {
+ off_t pos = lseek(G.output_fd, 0, SEEK_CUR);
+ if (pos != (off_t)-1)
+ ftruncate(G.output_fd, pos);
}
/* Draw full bar and free its resources */
FILE *dfp; /* socket to ftp server (data) */
char *proxy = NULL;
char *fname_out_alloc;
+ char *redirected_path = NULL;
struct host_info server;
struct host_info target;
/* -P DIR is considered only if there was no -O FILE */
if (G.dir_prefix)
G.fname_out = fname_out_alloc = concat_path_file(G.dir_prefix, G.fname_out);
+ else {
+ /* redirects may free target.path later, need to make a copy */
+ G.fname_out = fname_out_alloc = xstrdup(G.fname_out);
+ }
}
#if ENABLE_FEATURE_WGET_STATUSBAR
G.curfile = bb_get_last_path_component_nostrip(G.fname_out);
}
#endif
- if (G.beg_range)
+ if (G.beg_range != 0)
fprintf(sfp, "Range: bytes=%"OFF_FMT"u-\r\n", G.beg_range);
#if ENABLE_FEATURE_WGET_LONG_OPTIONS
fputs(G.extra_headers, sfp);
if (option_mask32 & WGET_OPT_POST_DATA) {
- char *estr = URL_escape(G.post_data);
fprintf(sfp,
"Content-Type: application/x-www-form-urlencoded\r\n"
"Content-Length: %u\r\n"
"\r\n"
"%s",
- (int) strlen(estr), estr
+ (int) strlen(G.post_data), G.post_data
);
- free(estr);
} else
#endif
{
(e.g. Boa/0.94.14rc21) simply use code 204 when file size is zero.
*/
case 204:
+ if (G.beg_range != 0) {
+ /* "Range:..." was not honored by the server.
+ * Restart download from the beginning.
+ */
+ reset_beg_range_to_zero();
+ }
break;
case 300: /* redirection */
case 301:
case 302:
case 303:
break;
- case 206:
- if (G.beg_range)
+ case 206: /* Partial Content */
+ if (G.beg_range != 0)
+ /* "Range:..." worked. Good. */
break;
+ /* Partial Content even though we did not ask for it??? */
/* fall through */
default:
bb_error_msg_and_die("server returned error: %s", sanitize_string(G.wget_buf));
bb_error_msg_and_die("too many redirections");
fclose(sfp);
if (str[0] == '/') {
- free(target.allocated);
- target.path = target.allocated = xstrdup(str+1);
+ free(redirected_path);
+ target.path = redirected_path = xstrdup(str+1);
/* lsa stays the same: it's on the same server */
} else {
parse_url(str, &target);
free(server.allocated);
free(target.allocated);
free(fname_out_alloc);
+ free(redirected_path);
}
int wget_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
"post-data\0" Required_argument "\xfd"
/* Ignored (we don't do ssl) */
"no-check-certificate\0" No_argument "\xfc"
+ /* Ignored (we don't support caching) */
+ "no-cache\0" No_argument "\xfb"
;
#endif