master xplshn/aruu / cmd / net / wget.c
  1/* see license file for copyright and license details */
  2
  3#include "arg.h"
  4#include "tls.h"
  5#include "util.h"
  6
  7#include <arpa/inet.h>
  8#include <ctype.h>
  9#include <errno.h>
 10#include <fcntl.h>
 11#include <netdb.h>
 12#include <netinet/in.h>
 13#include <stdio.h>
 14#include <stdlib.h>
 15#include <string.h>
 16#include <sys/socket.h>
 17#include <sys/stat.h>
 18#include <sys/types.h>
 19#include <unistd.h>
 20
 21struct Stream {
 22  struct TlsSocket *ts;
 23  char              buf[8192];
 24  size_t            len;
 25  size_t            idx;
 26};
 27
 28static int    qflag                = 0;
 29static int    Sflag                = 0;
 30static int    cflag                = 0;
 31static int    spider               = 0;
 32static int    no_check_certificate = 0;
 33static int    timeout_sec          = 900;
 34static char  *Pflag                = NULL;
 35static char  *Oflag                = NULL;
 36static char  *user_agent           = "wget/aruu";
 37static char  *post_data            = NULL;
 38static char  *post_file            = NULL;
 39static char **custom_headers       = NULL;
 40static size_t custom_headers_num   = 0;
 41
 42static void
 43usage(void)
 44{
 45  eprintf(
 46      "usage: %s [-cqS] [-O file] [-P dir] [-T timeout] [-U "
 47      "user_agent] "
 48      "[-post-data data] [-post-file file] [-header header] "
 49      "[-no-check-certificate] [-spider] url\n",
 50      argv0
 51  );
 52}
 53
 54static void
 55add_header(const char *hdr)
 56{
 57  custom_headers = ereallocarray(custom_headers, custom_headers_num + 1, sizeof(*custom_headers));
 58  custom_headers[custom_headers_num++] = estrdup(hdr);
 59}
 60
 61static int
 62dial(const char *host, const char *port)
 63{
 64  struct addrinfo hints, *res, *rp;
 65  int             fd = -1, r;
 66
 67  memset(&hints, 0, sizeof(hints));
 68  hints.ai_family   = AF_UNSPEC;
 69  hints.ai_socktype = SOCK_STREAM;
 70
 71  r = getaddrinfo(host, port, &hints, &res);
 72  if (r != 0) {
 73    if (!qflag)
 74      weprintf("getaddrinfo %s:%s: %s\n", host, port, gai_strerror(r));
 75    return -1;
 76  }
 77
 78  for (rp = res; rp; rp = rp->ai_next) {
 79    fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
 80    if (fd < 0)
 81      continue;
 82    if (timeout_sec > 0) {
 83      struct timeval tv;
 84      tv.tv_sec  = timeout_sec;
 85      tv.tv_usec = 0;
 86      setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
 87      setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
 88    }
 89    if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0)
 90      break;
 91    close(fd);
 92    fd = -1;
 93  }
 94
 95  freeaddrinfo(res);
 96  return fd;
 97}
 98
 99static void
100parse_url(char *url, char **host, char **port, char **path, int *is_tls)
101{
102  char *p, *ss;
103
104  *is_tls = 0;
105  if (strncasecmp(url, "http://", 7) == 0) {
106    url += 7;
107  } else if (strncasecmp(url, "https://", 8) == 0) {
108    url += 8;
109    *is_tls = 1;
110  } else {
111    eprintf("unsupported protocol or invalid url: %s\n", url);
112  }
113
114  *host = url;
115  p     = strchr(url, '/');
116  if (p) {
117    *p    = '\0';
118    *path = p + 1;
119  } else {
120    *path = "";
121  }
122
123  /* handle ipv6 brackets or host:port */
124  if (**host == '[') {
125    (*host)++;
126    ss = strchr(*host, ']');
127    if (ss) {
128      *ss = '\0';
129      ss++;
130      if (*ss == ':')
131        *port = ss + 1;
132      else
133        *port = *is_tls ? "443" : "80";
134    } else {
135      eprintf("invalid ipv6 literal: %s\n", *host);
136    }
137  } else {
138    p = strrchr(*host, ':');
139    if (p) {
140      *p    = '\0';
141      *port = p + 1;
142    } else {
143      *port = *is_tls ? "443" : "80";
144    }
145  }
146}
147
148static char *
149find_header(const char *headers, const char *name)
150{
151  const char *p;
152  size_t      len = strlen(name);
153
154  p = headers;
155  while (p && *p) {
156    if (strncasecmp(p, name, len) == 0) {
157      p += len;
158      while (*p == ' ' || *p == '\t')
159        p++;
160      len = strcspn(p, "\r\n");
161      return estrndup(p, len);
162    }
163    p = strchr(p, '\n');
164    if (p)
165      p++;
166  }
167  return NULL;
168}
169
170static int
171stream_getc(struct Stream *s)
172{
173  ssize_t r;
174
175  if (s->idx < s->len) {
176    return (unsigned char)s->buf[s->idx++];
177  }
178  s->idx = 0;
179  r      = tlss_read(s->ts, s->buf, sizeof(s->buf));
180  if (r <= 0) {
181    s->len = 0;
182    return EOF;
183  }
184  s->len = (size_t)r;
185  return (unsigned char)s->buf[s->idx++];
186}
187
188static size_t
189stream_read(struct Stream *s, void *ptr, size_t size)
190{
191  size_t  total = 0;
192  size_t  n;
193  char   *p = ptr;
194  ssize_t r;
195
196  while (total < size) {
197    if (s->idx < s->len) {
198      n = MIN(size - total, s->len - s->idx);
199      memcpy(p + total, s->buf + s->idx, n);
200      s->idx += n;
201      total += n;
202    } else {
203      s->idx = 0;
204      r      = tlss_read(s->ts, s->buf, sizeof(s->buf));
205      if (r <= 0) {
206        s->len = 0;
207        break;
208      }
209      s->len = (size_t)r;
210    }
211  }
212  return total;
213}
214
215static void
216read_chunked(struct Stream *s, int out_fd)
217{
218  char      line[128];
219  char      chunk_buf[8192];
220  size_t    line_len, n;
221  long long chunk_size, remaining;
222  int       c;
223
224  for (;;) {
225    line_len = 0;
226    for (;;) {
227      c = stream_getc(s);
228      if (c == EOF)
229        eprintf(
230            "unexpected end of file reading chunk "
231            "size\n"
232        );
233      if (c == '\n') {
234        line[line_len] = '\0';
235        break;
236      }
237      if (c != '\r' && line_len < sizeof(line) - 1) {
238        line[line_len++] = c;
239      }
240    }
241
242    chunk_size = strtoll(line, NULL, 16);
243    if (chunk_size == 0) {
244      stream_getc(s);
245      stream_getc(s);
246      break;
247    }
248
249    remaining = chunk_size;
250    while (remaining > 0) {
251      n = stream_read(s, chunk_buf, MIN(remaining, (long long)sizeof(chunk_buf)));
252      if (n == 0)
253        eprintf(
254            "unexpected end of file in chunk "
255            "data\n"
256        );
257      if (writeall(out_fd, chunk_buf, n) < 0)
258        eprintf("write output:\n");
259      remaining -= n;
260    }
261
262    stream_getc(s);
263    stream_getc(s);
264  }
265}
266
267static void
268read_non_chunked(struct Stream *s, int out_fd, long long content_len)
269{
270  char      chunk_buf[8192];
271  long long remaining = content_len;
272  size_t    n, to_read;
273
274  while (content_len < 0 || remaining > 0) {
275    to_read = sizeof(chunk_buf);
276    if (content_len >= 0)
277      to_read = (size_t)MIN(remaining, (long long)sizeof(chunk_buf));
278    n = stream_read(s, chunk_buf, to_read);
279    if (n == 0) {
280      if (content_len >= 0)
281        eprintf("unexpected end of file\n");
282      break;
283    }
284    if (writeall(out_fd, chunk_buf, n) < 0)
285      eprintf("write output:\n");
286    if (content_len >= 0)
287      remaining -= n;
288  }
289}
290
291static void
292req_printf(struct TlsSocket *ts, const char *fmt, ...)
293{
294  va_list ap;
295  char    buf[1024];
296  int     len;
297
298  va_start(ap, fmt);
299  len = vsnprintf(buf, sizeof(buf), fmt, ap);
300  va_end(ap);
301  if (len > 0)
302    tlss_write(ts, buf, len);
303}
304
305// ?man wget: retrieve files from the web
306// ?man arguments: url
307// ?man download files over http or https
308int
309main(int argc, char *argv[])
310{
311  struct Stream     s;
312  char             *url, *host, *port, *path, *loc;
313  char             *curr_host, *curr_port, *curr_path;
314  char             *new_url;
315  char             *cl_str;
316  char             *te_str;
317  char             *header_end;
318  char             *out_name;
319  int               redirects     = 0;
320  int               max_redirects = 20;
321  int               sock_fd       = -1;
322  int               out_fd        = 1;
323  int               chunked;
324  int               status;
325  long long         content_len;
326  size_t            total_read;
327  ssize_t           n;
328  size_t            dir_len;
329  char             *last_slash;
330  int               is_tls        = 0;
331  struct TlsSocket *tls_sock      = NULL;
332  off_t             resume_offset = 0;
333  int               out_mode      = O_WRONLY | O_CREAT | O_TRUNC;
334  long long         post_len      = 0;
335  int               post_fd       = -1;
336  size_t            i;
337
338  ARGBEGIN
339  {
340    // ?man -O:str: specify output file path
341    case 'O':
342      Oflag = EARGF(usage());
343      break;
344    // ?man -P:str: specify output directory prefix
345    case 'P':
346      Pflag = EARGF(usage());
347      break;
348    // ?man -T:num: set network read and connect timeout
349    case 'T':
350      timeout_sec = estrtonum(EARGF(usage()), 0, 100000);
351      break;
352    // ?man -U:str: set User-Agent header
353    case 'U':
354      user_agent = EARGF(usage());
355      break;
356    // ?man -c: continue retrieval of aborted transfer
357    case 'c':
358      cflag = 1;
359      break;
360    // ?man -q: quiet mode to suppress stderr output
361    case 'q':
362      qflag = 1;
363      break;
364    // ?man -S: print server response headers to stderr
365    case 'S':
366      Sflag = 1;
367      break;
368    // ?man --: specify - option
369    case '-':
370      if (strcmp(argv[0], "-no-check-certificate") == 0) {
371        no_check_certificate = 1;
372        brk_                 = 1;
373      } else if (strncmp(argv[0], "-header=", 8) == 0) {
374        add_header(argv[0] + 8);
375        brk_ = 1;
376      } else if (strcmp(argv[0], "-header") == 0) {
377        brk_ = 1;
378        if (!argv[1])
379          usage();
380        add_header(argv[1]);
381        argv++;
382        argc--;
383      } else if (strncmp(argv[0], "-post-data=", 11) == 0) {
384        post_data = argv[0] + 11;
385        brk_      = 1;
386      } else if (strcmp(argv[0], "-post-data") == 0) {
387        brk_ = 1;
388        if (!argv[1])
389          usage();
390        post_data = argv[1];
391        argv++;
392        argc--;
393      } else if (strncmp(argv[0], "-post-file=", 11) == 0) {
394        post_file = argv[0] + 11;
395        brk_      = 1;
396      } else if (strcmp(argv[0], "-post-file") == 0) {
397        brk_ = 1;
398        if (!argv[1])
399          usage();
400        post_file = argv[1];
401        argv++;
402        argc--;
403      } else if (strcmp(argv[0], "-spider") == 0) {
404        spider = 1;
405        brk_   = 1;
406      } else {
407        usage();
408      }
409      break;
410    default:
411      usage();
412  }
413  ARGEND
414
415  if (argc < 1)
416    usage();
417
418  url = estrdup(argv[0]);
419
420  /* determine output filename early to check for resume */
421  out_name = NULL;
422  if (Oflag) {
423    out_name = Oflag;
424  } else {
425    last_slash = strrchr(url, '/');
426    if (last_slash && *(last_slash + 1))
427      out_name = last_slash + 1;
428    else
429      out_name = "index.html";
430
431    if (Pflag) {
432      char *tmp = emalloc(strlen(Pflag) + 1 + strlen(out_name) + 1);
433      sprintf(tmp, "%s/%s", Pflag, out_name);
434      out_name = tmp;
435    }
436  }
437
438  if (cflag && out_name && strcmp(out_name, "-") != 0) {
439    struct stat st;
440    if (stat(out_name, &st) == 0 && S_ISREG(st.st_mode)) {
441      resume_offset = st.st_size;
442    }
443  }
444
445  if (post_data) {
446    post_len = strlen(post_data);
447  } else if (post_file) {
448    struct stat st;
449    post_fd = open(post_file, O_RDONLY);
450    if (post_fd < 0)
451      eprintf("open %s:\n", post_file);
452    if (fstat(post_fd, &st) < 0)
453      eprintf("stat %s:\n", post_file);
454    post_len = st.st_size;
455  }
456
457  while (!tls_sock) {
458    if (redirects > max_redirects)
459      eprintf("too many redirects\n");
460
461    curr_host = curr_port = curr_path = NULL;
462    parse_url(url, &curr_host, &curr_port, &curr_path, &is_tls);
463
464    host = estrdup(curr_host);
465    port = estrdup(curr_port);
466    path = estrdup(curr_path);
467
468    sock_fd = dial(host, port);
469    if (sock_fd < 0)
470      eprintf("failed to connect to %s:%s\n", host, port);
471
472    tls_sock = tlss_connect(sock_fd, host, !no_check_certificate, is_tls);
473    if (!tls_sock) {
474      close(sock_fd);
475      eprintf("failed to establish TLS connection with %s\n", host);
476    }
477
478    /* send http request */
479    const char *method = spider ? "HEAD" : ((post_data || post_file) ? "POST" : "GET");
480    req_printf(tls_sock, "%s /%s HTTP/1.1\r\n", method, path);
481    req_printf(tls_sock, "Host: %s\r\n", host);
482    req_printf(tls_sock, "User-Agent: %s\r\n", user_agent);
483    req_printf(tls_sock, "Connection: close\r\n");
484
485    if (resume_offset > 0) {
486      req_printf(tls_sock, "Range: bytes=%lld-\r\n", (long long)resume_offset);
487    }
488
489    if (post_data || post_file) {
490      int has_ct = 0;
491      for (i = 0; i < custom_headers_num; i++) {
492        if (strncasecmp(custom_headers[i], "Content-Type:", 13) == 0) {
493          has_ct = 1;
494          break;
495        }
496      }
497      if (!has_ct) {
498        req_printf(
499            tls_sock,
500            "Content-Type: "
501            "application/"
502            "x-www-form-urlencoded\r\n"
503        );
504      }
505      req_printf(tls_sock, "Content-Length: %lld\r\n", post_len);
506    }
507
508    for (i = 0; i < custom_headers_num; i++) {
509      req_printf(tls_sock, "%s\r\n", custom_headers[i]);
510    }
511
512    req_printf(tls_sock, "\r\n");
513
514    if (post_data) {
515      tlss_write(tls_sock, post_data, strlen(post_data));
516    } else if (post_file) {
517      char    io_buf[8192];
518      ssize_t r;
519      while ((r = read(post_fd, io_buf, sizeof(io_buf))) > 0) {
520        if (tlss_write(tls_sock, io_buf, r) < 0) {
521          eprintf("failed to write post data:\n");
522        }
523      }
524      close(post_fd);
525      post_fd = -1;
526    }
527
528    /* read headers */
529    total_read = 0;
530    header_end = NULL;
531    memset(s.buf, 0, sizeof(s.buf));
532    while (total_read < sizeof(s.buf) - 1) {
533      n = tlss_read(tls_sock, s.buf + total_read, sizeof(s.buf) - 1 - total_read);
534      if (n <= 0) {
535        if (n < 0)
536          eprintf("read socket:\n");
537        else
538          eprintf(
539              "connection closed by "
540              "server\n"
541          );
542      }
543      total_read += n;
544      s.buf[total_read] = '\0';
545      header_end        = strstr(s.buf, "\r\n\r\n");
546      if (header_end)
547        break;
548    }
549
550    if (!header_end)
551      eprintf("http header too large or not found\n");
552
553    *header_end = '\0';
554    s.ts        = tls_sock;
555    s.len       = total_read;
556    s.idx       = (header_end + 4) - s.buf;
557
558    if (Sflag) {
559      fprintf(stderr, "%s\n\n", s.buf);
560    }
561
562    if (strncasecmp(s.buf, "HTTP/1.1 ", 9) != 0 && strncasecmp(s.buf, "HTTP/1.0 ", 9) != 0) {
563      eprintf("invalid http response: %s\n", s.buf);
564    }
565    status = atoi(s.buf + 9);
566
567    if (status >= 300 && status < 400) {
568      loc = find_header(s.buf, "Location:");
569      if (!loc)
570        eprintf(
571            "redirect response without location "
572            "header\n"
573        );
574
575      if (strncasecmp(loc, "http://", 7) == 0 || strncasecmp(loc, "https://", 8) == 0) {
576        new_url = estrdup(loc);
577      } else if (loc[0] == '/') {
578        new_url = emalloc(8 + strlen(host) + strlen(port) + strlen(loc) + 2);
579        sprintf(new_url, "%s://%s:%s%s", is_tls ? "https" : "http", host, port, loc);
580      } else {
581        last_slash = strrchr(path, '/');
582        dir_len    = 0;
583        if (last_slash)
584          dir_len = last_slash - path + 1;
585        new_url = emalloc(8 + strlen(host) + strlen(port) + 1 + dir_len + strlen(loc) + 2);
586        sprintf(new_url, "%s://%s:%s/", is_tls ? "https" : "http", host, port);
587        if (dir_len > 0)
588          strncat(new_url, path, dir_len);
589        strcat(new_url, loc);
590      }
591
592      free(loc);
593      free(url);
594      url = new_url;
595      tlss_close(tls_sock, 1);
596      tls_sock = NULL;
597      redirects++;
598    } else if (status == 206) {
599      out_mode = O_WRONLY | O_CREAT | O_APPEND;
600    } else if (status == 200) {
601      out_mode = O_WRONLY | O_CREAT | O_TRUNC;
602    } else if (status == 416) {
603      if (!qflag)
604        weprintf(
605            "file already fully retrieved or "
606            "range invalid\n"
607        );
608      tlss_close(tls_sock, 1);
609      free(url);
610      free(host);
611      free(port);
612      free(path);
613      return 0;
614    } else {
615      eprintf("server returned status: %d\n", status);
616    }
617
618    free(host);
619    free(port);
620    free(path);
621  }
622
623  if (spider) {
624    tlss_close(tls_sock, 1);
625    free(url);
626    return 0;
627  }
628
629  cl_str      = find_header(s.buf, "Content-Length:");
630  content_len = -1;
631  if (cl_str) {
632    content_len = strtoll(cl_str, NULL, 10);
633    free(cl_str);
634  }
635
636  te_str  = find_header(s.buf, "Transfer-Encoding:");
637  chunked = 0;
638  if (te_str) {
639    if (strcasecmp(te_str, "chunked") == 0)
640      chunked = 1;
641    free(te_str);
642  }
643
644  if (strcmp(out_name, "-") != 0) {
645    out_fd = open(out_name, out_mode, 0644);
646    if (out_fd < 0)
647      eprintf("open %s:\n", out_name);
648  }
649
650  if (chunked)
651    read_chunked(&s, out_fd);
652  else
653    read_non_chunked(&s, out_fd, content_len);
654
655  tlss_close(tls_sock, 1);
656  if (out_fd != 1)
657    close(out_fd);
658  if (Oflag != out_name && Pflag)
659    free(out_name);
660  free(url);
661
662  for (i = 0; i < custom_headers_num; i++) {
663    free(custom_headers[i]);
664  }
665  free(custom_headers);
666
667  return 0;
668}