From 8f888fb1a3469b87e557efad93b293dd36288ba9 Mon Sep 17 00:00:00 2001 From: Sunil Nimmagadda Date: Sat, 14 Sep 2024 12:32:01 +0530 Subject: A HTTP(S), FTP client --- Makefile | 14 + authors | 1 + cmd.c | 637 ++++++++++++++++++++ file.c | 53 ++ ftp.1 | 411 +++++++++++++ ftp.c | 445 ++++++++++++++ ftp.h | 115 ++++ http.c | 833 ++++++++++++++++++++++++++ main.c | 526 ++++++++++++++++ progressmeter.c | 358 +++++++++++ regress/Makefile | 3 + regress/unit-tests/Makefile | 2 + regress/unit-tests/url_parse/Makefile | 17 + regress/unit-tests/url_parse/test_url_parse.c | 134 +++++ url.c | 424 +++++++++++++ util.c | 215 +++++++ xmalloc.c | 147 +++++ xmalloc.h | 41 ++ 18 files changed, 4376 insertions(+) create mode 100644 Makefile create mode 100644 authors create mode 100644 cmd.c create mode 100644 file.c create mode 100644 ftp.1 create mode 100644 ftp.c create mode 100644 ftp.h create mode 100644 http.c create mode 100644 main.c create mode 100644 progressmeter.c create mode 100644 regress/Makefile create mode 100644 regress/unit-tests/Makefile create mode 100644 regress/unit-tests/url_parse/Makefile create mode 100644 regress/unit-tests/url_parse/test_url_parse.c create mode 100644 url.c create mode 100644 util.c create mode 100644 xmalloc.c create mode 100644 xmalloc.h diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d8d5906 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +# Define SMALL to disable command line editing +#CFLAGS+=-DSMALL + +PROG= ftp +SRCS= cmd.c file.c ftp.c http.c main.c progressmeter.c url.c util.c xmalloc.c + +LDADD+= -ledit -lcurses -lutil -ltls -lssl -lcrypto +DPADD+= ${LIBEDIT} ${LIBCURSES} ${LIBUTIL} ${LIBTLS} ${LIBSSL} ${LIBCRYPTO} + +regression-tests: + @echo Running regression tests... + @cd ${.CURDIR}/regress && ${MAKE} depend && exec ${MAKE} regress + +.include diff --git a/authors b/authors new file mode 100644 index 0000000..417bc71 --- /dev/null +++ b/authors @@ -0,0 +1 @@ +Sunil Nimmagadda diff --git a/cmd.c b/cmd.c new file mode 100644 index 0000000..faf7949 --- /dev/null +++ b/cmd.c @@ -0,0 +1,637 @@ +/* + * Copyright (c) 2018 Sunil Nimmagadda + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ftp.h" + +#define ARGVMAX 64 + +static void cmd_interrupt(int); +static int cmd_lookup(const char *); +static FILE *data_fopen(const char *); +static void do_open(int, char **); +static void do_help(int, char **); +static void do_quit(int, char **); +static void do_ls(int, char **); +static void do_pwd(int, char **); +static void do_cd(int, char **); +static void do_get(int, char **); +static void do_passive(int, char **); +static void do_lcd(int, char **); +static void do_lpwd(int, char **); +static void do_put(int, char **); +static void do_mget(int, char **); +static void ftp_abort(void); +static char *prompt(void); + +static FILE *ctrl_fp, *data_fp; + +static struct { + const char *name; + const char *info; + void (*cmd)(int, char **); + int conn_required; +} cmd_tbl[] = { + { "open", "connect to remote ftp server", do_open, 0 }, + { "close", "terminate ftp session", do_quit, 1 }, + { "help", "print local help information", do_help, 0 }, + { "?", "print local help information", do_help, 0 }, + { "quit", "terminate ftp session and exit", do_quit, 0 }, + { "exit", "terminate ftp session and exit", do_quit, 0 }, + { "ls", "list contents of remote directory", do_ls, 1 }, + { "pwd", "print working directory on remote machine", do_pwd, 1 }, + { "cd", "change remote working directory", do_cd, 1 }, + { "nlist", "nlist contents of remote directory", do_ls, 1 }, + { "get", "receive file", do_get, 1 }, + { "passive", "toggle passive transfer mode", do_passive, 0 }, + { "lcd", "change local working directory", do_lcd, 0 }, + { "lpwd", "print local working directory", do_lpwd, 0 }, + { "put", "send one file", do_put, 1 }, + { "mget", "get multiple files", do_mget, 1 }, + { "mput", "send multiple files", do_mget, 1 }, +}; + +static void +cmd_interrupt(int signo) +{ + const char msg[] = "\rwaiting for remote to finish abort\n"; + int save_errno = errno; + + if (data_fp != NULL) + (void)write(STDERR_FILENO, msg, sizeof(msg) - 1); + + interrupted = 1; + errno = save_errno; +} + +void +cmd(const char *host, const char *port, const char *path) +{ + HistEvent hev; + EditLine *el; + History *hist; + const char *line; + char **ap, *argv[ARGVMAX], *cp; + int count, i; + + if ((el = el_init(getprogname(), stdin, stdout, stderr)) == NULL) + err(1, "couldn't initialise editline"); + + if ((hist = history_init()) == NULL) + err(1, "couldn't initialise editline history"); + + history(hist, &hev, H_SETSIZE, 100); + el_set(el, EL_HIST, history, hist); + el_set(el, EL_PROMPT, prompt); + el_set(el, EL_EDITOR, "emacs"); + el_set(el, EL_TERMINAL, NULL); + el_set(el, EL_SIGNAL, 1); + el_source(el, NULL); + + if (host != NULL) { + argv[0] = "open"; + argv[1] = (char *)host; + argv[2] = port ? (char *)port : "21"; + do_open(3, argv); + /* If we don't have a connection, exit */ + if (ctrl_fp == NULL) + exit(1); + + if (path != NULL) { + argv[0] = "cd"; + argv[1] = (char *)path; + do_cd(2, argv); + } + } + + for (;;) { + signal(SIGINT, SIG_IGN); + if ((line = el_gets(el, &count)) == NULL || count <= 0) { + if (verbose) + fprintf(stderr, "\n"); + argv[0] = "quit"; + do_quit(1, argv); + break; + } + + if (count <= 1) + continue; + + if ((cp = strrchr(line, '\n')) != NULL) + *cp = '\0'; + + history(hist, &hev, H_ENTER, line); + for (ap = argv; ap < &argv[ARGVMAX - 1] && + (*ap = strsep((char **)&line, " \t")) != NULL;) { + if (**ap != '\0') + ap++; + } + *ap = NULL; + + if (argv[0] == NULL) + continue; + + if ((i = cmd_lookup(argv[0])) == -1) { + fprintf(stderr, "Invalid command.\n"); + continue; + } + + if (cmd_tbl[i].conn_required && ctrl_fp == NULL) { + fprintf(stderr, "Not connected.\n"); + continue; + } + + interrupted = 0; + signal(SIGINT, cmd_interrupt); + cmd_tbl[i].cmd(ap - argv, argv); + + if (strcmp(cmd_tbl[i].name, "quit") == 0 || + strcmp(cmd_tbl[i].name, "exit") == 0) + break; + } + + el_end(el); +} + +static int +cmd_lookup(const char *cmd) +{ + size_t i; + + for (i = 0; i < nitems(cmd_tbl); i++) + if (strcmp(cmd, cmd_tbl[i].name) == 0) + return i; + + return -1; +} + +static char * +prompt(void) +{ + return "ftp> "; +} + +static FILE * +data_fopen(const char *mode) +{ + int fd; + + fd = activemode ? ftp_eprt(ctrl_fp) : ftp_epsv(ctrl_fp); + if (fd == -1) { + if (io_debug) + fprintf(stderr, "Failed to open data connection"); + + return NULL; + } + + return fdopen(fd, mode); +} + +static void +ftp_abort(void) +{ + char buf[BUFSIZ]; + + snprintf(buf, sizeof buf, "%c%c%c", IAC, IP, IAC); + if (send(fileno(ctrl_fp), buf, 3, MSG_OOB) != 3) + warn("abort"); + + ftp_command(ctrl_fp, "%cABOR", DM); +} + +static void +do_open(int argc, char **argv) +{ + const char *host = NULL, *port = "21"; + char *buf = NULL; + size_t n = 0; + int sock; + + if (ctrl_fp != NULL) { + fprintf(stderr, "already connected, use close first.\n"); + return; + } + + switch (argc) { + case 3: + port = argv[2]; + /* FALLTHROUGH */ + case 2: + host = argv[1]; + break; + default: + fprintf(stderr, "usage: open host [port]\n"); + return; + } + + if ((sock = tcp_connect(host, port, 0)) == -1) + return; + + fprintf(stderr, "Connected to %s.\n", host); + if ((ctrl_fp = fdopen(sock, "r+")) == NULL) + err(1, "%s: fdopen", __func__); + + /* greeting */ + ftp_getline(&buf, &n, 0, ctrl_fp); + free(buf); + if (ftp_auth(ctrl_fp, NULL, NULL) != P_OK) { + fclose(ctrl_fp); + ctrl_fp = NULL; + } +} + +static void +do_help(int argc, char **argv) +{ + size_t i; + int j; + + if (argc == 1) { + for (i = 0; i < nitems(cmd_tbl); i++) + fprintf(stderr, "%s\n", cmd_tbl[i].name); + + return; + } + + for (i = 1; i < (size_t)argc; i++) { + if ((j = cmd_lookup(argv[i])) == -1) + fprintf(stderr, "invalid help command %s\n", argv[i]); + else + fprintf(stderr, "%s\t%s\n", argv[i], cmd_tbl[j].info); + } +} + +static void +do_quit(int argc, char **argv) +{ + if (ctrl_fp == NULL) + return; + + ftp_command(ctrl_fp, "QUIT"); + fclose(ctrl_fp); + ctrl_fp = NULL; +} + +static void +do_ls(int argc, char **argv) +{ + FILE *dst_fp = stdout; + const char *cmd, *local_fname = NULL, *remote_dir = NULL; + char *buf = NULL; + size_t n = 0; + ssize_t len; + int r; + + switch (argc) { + case 3: + if (strcmp(argv[2], "-") != 0) + local_fname = argv[2]; + /* FALLTHROUGH */ + case 2: + remote_dir = argv[1]; + /* FALLTHROUGH */ + case 1: + break; + default: + fprintf(stderr, "usage: ls [remote-directory [local-file]]\n"); + return; + } + + if ((data_fp = data_fopen("r")) == NULL) + return; + + if (local_fname && (dst_fp = fopen(local_fname, "w")) == NULL) { + warn("fopen %s", local_fname); + fclose(data_fp); + data_fp = NULL; + return; + } + + cmd = (strcmp(argv[0], "ls") == 0) ? "LIST" : "NLST"; + if (remote_dir != NULL) + r = ftp_command(ctrl_fp, "%s %s", cmd, remote_dir); + else + r = ftp_command(ctrl_fp, "%s", cmd); + + if (r != P_PRE) { + fclose(data_fp); + data_fp = NULL; + if (dst_fp != stdout) + fclose(dst_fp); + + return; + } + + while ((len = getline(&buf, &n, data_fp)) != -1 && !interrupted) { + buf[len - 1] = '\0'; + if (len >= 2 && buf[len - 2] == '\r') + buf[len - 2] = '\0'; + + fprintf(dst_fp, "%s\n", buf); + } + + if (interrupted) + ftp_abort(); + + fclose(data_fp); + data_fp = NULL; + ftp_getline(&buf, &n, 0, ctrl_fp); + free(buf); + if (dst_fp != stdout) + fclose(dst_fp); +} + +static void +do_get(int argc, char **argv) +{ + FILE *dst_fp; + const char *local_fname = NULL, *p, *remote_fname; + char *buf = NULL; + size_t n = 0; + off_t file_sz, offset = 0; + + switch (argc) { + case 3: + local_fname = argv[2]; + /* FALLTHROUGH */ + case 2: + remote_fname = argv[1]; + break; + default: + fprintf(stderr, "usage: get remote-file [local-file]\n"); + return; + } + + if (local_fname == NULL) + local_fname = remote_fname; + + if (ftp_command(ctrl_fp, "TYPE I") != P_OK) + return; + + log_info("local: %s remote: %s\n", local_fname, remote_fname); + if (ftp_size(ctrl_fp, remote_fname, &file_sz, &buf) != P_OK) { + fprintf(stderr, "%s", buf); + return; + } + + if ((data_fp = data_fopen("r")) == NULL) + return; + + if ((dst_fp = fopen(local_fname, "w")) == NULL) { + warn("%s", local_fname); + fclose(data_fp); + data_fp = NULL; + return; + } + + if (ftp_command(ctrl_fp, "RETR %s", remote_fname) != P_PRE) { + fclose(data_fp); + data_fp = NULL; + fclose(dst_fp); + return; + } + + if (progressmeter) { + p = basename(remote_fname); + start_progress_meter(p, NULL, file_sz, &offset); + } + + copy_file(dst_fp, data_fp, &offset); + if (progressmeter) + stop_progress_meter(); + + if (interrupted) + ftp_abort(); + + fclose(data_fp); + data_fp = NULL; + fclose(dst_fp); + ftp_getline(&buf, &n, 0, ctrl_fp); + free(buf); +} + +static void +do_pwd(int argc, char **argv) +{ + ftp_command(ctrl_fp, "PWD"); +} + +static void +do_cd(int argc, char **argv) +{ + if (argc != 2) { + fprintf(stderr, "usage: cd remote-directory\n"); + return; + } + + ftp_command(ctrl_fp, "CWD %s", argv[1]); +} + +static void +do_passive(int argc, char **argv) +{ + switch (argc) { + case 1: + break; + case 2: + if (strcmp(argv[1], "on") == 0 || strcmp(argv[1], "off") == 0) + break; + + /* FALLTHROUGH */ + default: + fprintf(stderr, "usage: passive [on | off]\n"); + return; + } + + if (argv[1] != NULL) { + activemode = (strcmp(argv[1], "off") == 0) ? 1 : 0; + fprintf(stderr, "passive mode is %s\n", argv[1]); + return; + } + + activemode = !activemode; + fprintf(stderr, "passive mode is %s\n", activemode ? "off" : "on"); +} + +static void +do_lcd(int argc, char **argv) +{ + struct passwd *pw = NULL; + const char *dir, *login; + char cwd[PATH_MAX]; + + switch (argc) { + case 1: + case 2: + break; + default: + fprintf(stderr, "usage: lcd [local-directory]\n"); + return; + } + + if ((login = getlogin()) != NULL) + pw = getpwnam(login); + + if (pw == NULL && (pw = getpwuid(getuid())) == NULL) { + fprintf(stderr, "Failed to get home directory\n"); + return; + } + + dir = argv[1] ? argv[1] : pw->pw_dir; + if (chdir(dir) != 0) { + warn("local: %s", dir); + return; + } + + if (getcwd(cwd, sizeof cwd) == NULL) { + warn("getcwd"); + return; + } + + fprintf(stderr, "Local directory now %s\n", cwd); +} + +static void +do_lpwd(int argc, char **argv) +{ + char cwd[PATH_MAX]; + + if (getcwd(cwd, sizeof cwd) == NULL) { + warn("getcwd"); + return; + } + + fprintf(stderr, "Local directory %s\n", cwd); +} + +static void +do_put(int argc, char **argv) +{ + struct stat sb; + FILE *src_fp; + const char *local_fname, *p, *remote_fname = NULL; + char *buf = NULL; + size_t n = 0; + off_t file_sz, offset = 0; + + switch (argc) { + case 3: + remote_fname = argv[2]; + /* FALLTHROUGH */ + case 2: + local_fname = argv[1]; + break; + default: + fprintf(stderr, "usage: put local-file [remote-file]\n"); + return; + } + + if (remote_fname == NULL) + remote_fname = local_fname; + + if (ftp_command(ctrl_fp, "TYPE I") != P_OK) + return; + + log_info("local: %s remote: %s\n", local_fname, remote_fname); + if ((data_fp = data_fopen("w")) == NULL) + return; + + if ((src_fp = fopen(local_fname, "r")) == NULL) { + warn("%s", local_fname); + fclose(data_fp); + data_fp = NULL; + return; + } + + if (fstat(fileno(src_fp), &sb) != 0) { + warn("%s", local_fname); + fclose(data_fp); + data_fp = NULL; + fclose(src_fp); + return; + } + file_sz = sb.st_size; + + if (ftp_command(ctrl_fp, "STOR %s", remote_fname) != P_PRE) { + fclose(data_fp); + data_fp = NULL; + fclose(src_fp); + return; + } + + if (progressmeter) { + p = basename(remote_fname); + start_progress_meter(p, NULL, file_sz, &offset); + } + + copy_file(data_fp, src_fp, &offset); + if (progressmeter) + stop_progress_meter(); + + if (interrupted) + ftp_abort(); + + fclose(data_fp); + data_fp = NULL; + fclose(src_fp); + ftp_getline(&buf, &n, 0, ctrl_fp); + free(buf); +} + +static void +do_mget(int argc, char **argv) +{ + void (*fn)(int, char **); + const char *usage; + char *args[2]; + int i; + + if (strcmp(argv[0], "mget") == 0) { + fn = do_get; + args[0] = "get"; + usage = "mget remote-files"; + } else { + fn = do_put; + args[0] = "put"; + usage = "mput local-files"; + } + + if (argc == 1) { + fprintf(stderr, "usage: %s\n", usage); + return; + } + + for (i = 1; i < argc && !interrupted; i++) { + args[1] = argv[i]; + fn(2, args); + } +} diff --git a/file.c b/file.c new file mode 100644 index 0000000..f60f223 --- /dev/null +++ b/file.c @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2015 Sunil Nimmagadda + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include +#include + +#include "ftp.h" + +static FILE *src_fp; + +struct url * +file_get(struct url *url, off_t *offset, off_t *sz) +{ + struct stat sb; + int src_fd; + + if ((src_fd = fd_request(url->path, O_RDONLY, NULL)) == -1) + err(1, "Can't open file %s", url->path); + + if (fstat(src_fd, &sb) == 0) + *sz = sb.st_size; + + if ((src_fp = fdopen(src_fd, "r")) == NULL) + err(1, "%s: fdopen", __func__); + + if (*offset && fseeko(src_fp, *offset, SEEK_SET) == -1) + err(1, "%s: fseeko", __func__); + + return url; +} + +void +file_save(struct url *url, FILE *dst_fp, off_t *offset) +{ + copy_file(dst_fp, src_fp, offset); + fclose(src_fp); +} diff --git a/ftp.1 b/ftp.1 new file mode 100644 index 0000000..f610a80 --- /dev/null +++ b/ftp.1 @@ -0,0 +1,411 @@ +.\" $OpenBSD: ftp.1,v 1.114 2019/05/15 11:53:22 kmos Exp $ +.\" The Regents of the University of California. All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" 3. Neither the name of the University nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.\" @(#)ftp.1 8.3 (Berkeley) 10/9/94 +.\" +.\" Copyright (c) 2015 Sunil Nimmagadda +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\" +.Dd $Mdocdate: August 13 2015 $ +.Dt FTP 1 +.Os +.Sh NAME +.Nm ftp +.Nd Internet file transfer program +.Sh SYNOPSIS +.Nm +.Op Fl 46AVv +.Op Fl N Ar name +.Op Fl D Ar title +.Op Ar host Op Ar port +.Nm +.Op Fl 46ACMmVv +.Op Fl N Ar name +.Op Fl D Ar title +.Op Fl o Ar output +.Op Fl S Ar tls_options +.Op Fl U Ar useragent +.Op Fl w Ar seconds +.Ar url ... +.Sh DESCRIPTION +.Nm +is the user interface to the Internet standard File Transfer +Protocol (FTP). +The program allows a user to transfer files to and from a +remote network site. +.Pp +The latter usage format will fetch a file using either the +FTP, HTTP or HTTPS protocols into the current directory. +This is ideal for scripts. +Refer to +.Sx AUTO-FETCHING FILES +below for more information. +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl 4 +Forces +.Nm +to use IPv4 addresses only. +.It Fl 6 +Forces +.Nm +to use IPv6 addreses only. +.It Fl A +Force active mode FTP. +By default, +.Nm +will try to use passive mode FTP and fall back to active mode +if passive is not supported by the server. +This option causes +.Nm +to always use an active connection. +It is only useful for connecting +to very old servers that do not implement passive mode properly. +.It Fl C +Continue a previously interrupted file transfer. +.Nm +will continue transferring from an offset equal to the length of file. +.Pp +Resuming HTTP(S) transfers are only supported if the remote server supports the +.Dq Range +header. +.It Fl D Ar title +Specify a short title for the start of the progress bar. +.It Fl M +Causes +.Nm +to never display the progress meter in cases where it would do so by default. +.It Fl N Ar name +Use this alternative name instead of +.Nm +in some error reports. +.It Fl m +Causes +.Nm +to display the progress meter in cases where it would not do so by default. +.It Fl o Ar output +When fetching a file or URL, save the contents in +.Ar output . +To make the contents go to stdout, use `-' for +.Ar output . +.It Fl S Ar tls_options +TLS options to use with HTTPS transfers. +The following settings are available: +.Bl -tag -width Ds +.It Cm cafile Ns = Ns Ar /path/to/cert.pem +PEM encoded file containing CA certificates used for certificate validation. +.It Cm capath Ns = Ns Ar /path/to/certs/ +Directory containing PEM encoded CA certificates used for certificate +validation. +.It Cm ciphers Ns = Ns Ar cipher_list +Specify the list of ciphers that will be used by +.Nm . +See the +.Xr openssl 1 +.Cm ciphers +subcommand. +.It Cm depth Ns = Ns Ar max_depth +Maximum depth of the certificate chain allowed when performing validation. +.It Cm dont +Don't perform server certificate validation. +.It Cm protocols Ns = Ns Ar string +Specify the TLS protocols to use. +If not specified the value +.Qq all +is used. +Refer to the +.Xr tls_config_parse_protocols 3 +function for other valid protocol string values. +.It Cm muststaple +Require the server to present a valid OCSP stapling in the TLS handshake. +.It Cm noverifytime +Disable validation of certificate times and OCSP validation. +.It Cm session Ns = Ns Ar /path/to/session +Specify a file to use for TLS session data. +If this file has a non-zero length, the session data will be read from this file +and the client will attempt to resume the TLS session with the server. +Upon completion of a successful TLS handshake this file will be updated with +new session data, if available. +This file will be created if it does not already exist. +.El +.Pp +By default, server certificate validation is performed, and if it fails +.Nm +will abort. +If no +.Cm cafile +or +.Cm capath +setting is provided, +.Pa /etc/ssl/cert.pem +will be used. +.It Fl U Ar useragent +Set +.Ar useragent +as the User-Agent for HTTP(S) URL requests. +If not specified, the default User-Agent is +.Dq OpenBSD ftp . +.It Fl V +Disable verbose mode. +.It Fl v +Enable verbose mode. +This is the default if input if from a terminal. +Forces +.Nm +to show all responses from the remote server, as well as report on data +transfer statistics. +.It Fl w Ar seconds +Abort a slow connection after +.Ar seconds . +.El +.Pp +The host with which +.Nm +is to communicate may be specified on the command line. +If this is done, +.Nm +will immediately attempt to establish a connection to an +FTP server on that host; otherwise, +.Nm +will enter its command interpreter and await instructions +from the user. +When +.Nm +is awaiting commands, the prompt +.Dq ftp\*(Gt +is provided to the user. +The following commands are recognized +by +.Nm : +.Bl -tag -width Ds +.It Ic open Ar host Op Ar port +Establish a connection to the specified +.Ar host +FTP server. +An optional port number may be supplied, +in which case +.Nm +will attempt to contact an FTP server at that port. +.It Ic close +Terminate the FTP session with the remote server and +return to the command interpreter. +.It Ic help Op Ar command +Print an informative message about the meaning of +.Ar command . +If no argument is given, +.Nm +prints a list of the known commands. +.It Ic \&? Op Ar command +A synonym for +.Ic help . +.It Ic quit +Terminate the FTP session with the remote server and exit +.Nm . +.It Ic exit +A synonym for +.Ic quit . +.It Ic ls Op Ar remote-directory Op Ar local-file +Print a listing of the contents of a directory on the remote machine. +The listing includes any system-dependent information that the server +chooses to include; for example, most +.Ux +systems will produce output from the command +.Ql ls -l . +If +.Ar remote-directory +is left unspecified, the current working directory is used. +If no local file is specified, or if +.Ar local-file +is +.Sq - , +the output is sent to the terminal. +.It Ic nlist Op Ar remote-directory Op Ar local-file +Print a list of the files in a +directory on the remote machine. +If +.Ar remote-directory +is left unspecified, the current working directory is used. +If no local file is specified, or if +.Ar local-file +is +.Sq - , +the output is sent to the terminal. +Note that on some servers, the +.Ic nlist +command will only return information on normal files (not directories +or special files). +.It Ic pwd +Print the name of the current working directory on the remote +machine. +.It Ic cd Ar remote-directory +Change the working directory on the remote machine +to +.Ar remote-directory . +.It Ic get Ar remote-file Op Ar local-file +Retrieve the +.Ar remote-file +and store it on the local machine. +If the local +file name is not specified, it is given the same +name it has on the remote machine. +.It Ic passive Op Ic on | off +Toggle passive mode. +If passive mode is turned on (default is on), +.Nm +will send a +.Dv EPSV +command for all data connections instead of the usual +.Dv EPRT +command. +The +.Dv EPSV +command requests that the remote server open a port for the data connection +and return the address of that port. +The remote server listens on that port and the client connects to it. +When using the more traditional +.Dv EPRT +command, the client listens on a port and sends that address to the remote +server, who connects back to it. +Passive mode is useful when using +.Nm +through a gateway router or host that controls the directionality of +traffic. +.It Ic lcd Op Ar local-directory +Change the working directory on the local machine. +If +no +.Ar local-directory +is specified, the user's home directory is used. +.It Ic lpwd +Print the working directory on the local machine. +.It Ic put Ar local-file Op Ar remote-file +Store a local file on the remote machine. +If +.Ar remote-file +is left unspecified, the local file name is used. +.It Ic mget Ar remote-files +Do a +.Ic get +for each file name specified. +.It Ic mput Ar local-files +Do a +.Ic put +for each file name specified. +.El +.Sh AUTO-FETCHING FILES +In addition to standard commands, this version of +.Nm +supports an auto-fetch feature. +To enable auto-fetch, simply pass the list of hostnames/files +on the command line. +.Pp +The following formats are valid syntax for an auto-fetch element: +.Bl -tag -width Ds +.Sm off +.It Xo ftp:// +.Ar host Op : Ar port +.No / Ar file +.Xc +.Sm on +An FTP URL, retrieved using the FTP protocol if +.Ev ftp_proxy +isn't defined. +Otherwise, transfer using HTTP via the proxy defined in +.Ev ftp_proxy . +.Sm off +.It Xo http:// +.Ar host Op : Ar port +.No / Ar file +.Xc +.Sm on +An HTTP URL, retrieved using the HTTP protocol. +If +.Ev http_proxy +is defined, it is used as a URL to an HTTP proxy server. +.Sm off +.It Xo https:// +.Ar host Op : Ar port +.No / Ar file +.Xc +.Sm on +An HTTPS URL, retrieved using the HTTPS protocol. +If +.Ev http_proxy +is defined, this HTTPS proxy server will be used to fetch the +file using the CONNECT method. +.It Pf file: Ar file +.Ar file +is retrieved from a mounted file system. +.El +.Sh ENVIRONMENT +.Nm +utilizes the following environment variables: +.Bl -tag -width Ds +.It Ev ftp_proxy +URL of FTP proxy to use when making FTP URL requests +(if not defined, use the standard FTP protocol). +.It Ev http_proxy +URL of HTTP proxy to use when making HTTP(S) URL requests. +.El +.Sh PORT ALLOCATION +For active mode data connections, +.Nm +will listen to a random high TCP port. +The interval of ports used are configurable using +.Xr sysctl 8 +variables +.Va net.inet.ip.porthifirst +and +.Va net.inet.ip.porthilast . +.Sh HISTORY +The +.Nm +command first appeard in +.Bx 4.2 . +A complete rewrite of the +.Nm +command first appeared in +.Ox x.x . +.Sh AUTHORS +.An Sunil Nimmagadda Aq Mt sunil@openbsd.org +.Sh CAVEATS +While aborting a data transfer, certain FTP servers violate +the protocol by not responding with a 426 reply first, thereby making +.Nm +wait indefinitely for a correct reply. diff --git a/ftp.c b/ftp.c new file mode 100644 index 0000000..c0d9c89 --- /dev/null +++ b/ftp.c @@ -0,0 +1,445 @@ +/* + * Copyright (c) 2015 Sunil Nimmagadda + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ftp.h" +#include "xmalloc.h" + +static FILE *ctrl_fp; +static int data_fd; + +void +ftp_connect(struct url *url, int timeout) +{ + char *buf = NULL; + size_t n = 0; + int sock; + + if ((sock = tcp_connect(url->host, url->port, timeout)) == -1) + exit(1); + + if ((ctrl_fp = fdopen(sock, "r+")) == NULL) + err(1, "%s: fdopen", __func__); + + /* greeting */ + if (ftp_getline(&buf, &n, 0, ctrl_fp) != P_OK) { + warnx("Can't connect to host `%s'", url->host); + ftp_command(ctrl_fp, "QUIT"); + exit(1); + } + + free(buf); + log_info("Connected to %s\n", url->host); + if (ftp_auth(ctrl_fp, NULL, NULL) != P_OK) { + warnx("Can't login to host `%s'", url->host); + ftp_command(ctrl_fp, "QUIT"); + exit(1); + } +} + +struct url * +ftp_get(struct url *url, off_t *offset, off_t *sz) +{ + char *buf = NULL, *dir, *file; + + log_info("Using binary mode to transfer files.\n"); + if (ftp_command(ctrl_fp, "TYPE I") != P_OK) + errx(1, "Failed to set mode to binary"); + + dir = dirname(url->path); + if (ftp_command(ctrl_fp, "CWD %s", dir) != P_OK) + errx(1, "CWD command failed"); + + log_info("Retrieving %s\n", url->path); + file = basename(url->path); + if (oarg && strcmp(oarg, "-") == 0) + log_info("remote: %s\n", file); + else + log_info("local: %s remote: %s\n", oarg ? oarg : file , file); + + if (ftp_size(ctrl_fp, file, sz, &buf) != P_OK) { + fprintf(stderr, "%s", buf); + ftp_command(ctrl_fp, "QUIT"); + exit(1); + } + free(buf); + + if (activemode) + data_fd = ftp_eprt(ctrl_fp); + else if ((data_fd = ftp_epsv(ctrl_fp)) == -1) + data_fd = ftp_eprt(ctrl_fp); + + if (data_fd == -1) + errx(1, "Failed to establish data connection"); + + if (*offset && ftp_command(ctrl_fp, "REST %lld", *offset) != P_INTER) + errx(1, "REST command failed"); + + if (ftp_command(ctrl_fp, "RETR %s", file) != P_PRE) { + ftp_command(ctrl_fp, "QUIT"); + exit(1); + } + + return url; +} + +void +ftp_save(struct url *url, FILE *dst_fp, off_t *offset) +{ + struct sockaddr_storage ss; + FILE *data_fp; + socklen_t len; + int s; + + if (activemode) { + len = sizeof(ss); + if ((s = accept(data_fd, (struct sockaddr *)&ss, &len)) == -1) + err(1, "%s: accept", __func__); + + close(data_fd); + data_fd = s; + } + + if ((data_fp = fdopen(data_fd, "r")) == NULL) + err(1, "%s: fdopen data_fd", __func__); + + copy_file(dst_fp, data_fp, offset); + fclose(data_fp); +} + +void +ftp_close(struct url *url) +{ + char *buf = NULL; + size_t n = 0; + + /* + * Reading reply here after progressmeter stops. + */ + if (ftp_getline(&buf, &n, 0, ctrl_fp) != P_OK) + errx(1, "%s: %s", __func__, buf); + + free(buf); + ftp_command(ctrl_fp, "QUIT"); + fclose(ctrl_fp); +} + +int +ftp_getline(char **lineptr, size_t *n, int suppress_output, FILE *fp) +{ + ssize_t len; + char *bufp, code[4]; + const char *errstr; + int lookup[] = { P_PRE, P_OK, P_INTER, N_TRANS, N_PERM }; + + + if ((len = getline(lineptr, n, fp)) == -1) + err(1, "%s: getline", __func__); + + bufp = *lineptr; + if (!suppress_output) + log_info("%s", bufp); + + if (len < 4) + errx(1, "%s: line too short", __func__); + + (void)strlcpy(code, bufp, sizeof code); + if (bufp[3] == ' ') + goto done; + + /* multi-line reply */ + while (!(strncmp(code, bufp, 3) == 0 && bufp[3] == ' ')) { + if ((len = getline(lineptr, n, fp)) == -1) + err(1, "%s: getline", __func__); + + bufp = *lineptr; + if (!suppress_output) + log_info("%s", bufp); + + if (len < 4) + continue; + } + + done: + (void)strtonum(code, 100, 553, &errstr); + if (errstr) + errx(1, "%s: Response code is %s: %s", __func__, errstr, code); + + return lookup[code[0] - '1']; +} + +int +ftp_command(FILE *fp, const char *fmt, ...) +{ + va_list ap; + char *buf = NULL, *cmd; + size_t n = 0; + int r; + + va_start(ap, fmt); + r = vasprintf(&cmd, fmt, ap); + va_end(ap); + if (r < 0) + errx(1, "%s: vasprintf", __func__); + + if (io_debug) + fprintf(stderr, ">>> %s\n", cmd); + + if (fprintf(fp, "%s\r\n", cmd) < 0) + errx(1, "%s: fprintf", __func__); + + (void)fflush(fp); + free(cmd); + r = ftp_getline(&buf, &n, 0, fp); + free(buf); + return r; + +} + +int +ftp_auth(FILE *fp, const char *user, const char *pass) +{ + char *addr = NULL, hn[HOST_NAME_MAX+1], *un; + int code; + + code = ftp_command(fp, "USER %s", user ? user : "anonymous"); + if (code != P_OK && code != P_INTER) + return code; + + if (pass == NULL) { + if (gethostname(hn, sizeof hn) == -1) + err(1, "%s: gethostname", __func__); + + un = getlogin(); + xasprintf(&addr, "%s@%s", un ? un : "anonymous", hn); + } + + code = ftp_command(fp, "PASS %s", pass ? pass : addr); + free(addr); + return code; +} + +int +ftp_size(FILE *fp, const char *fn, off_t *sizep, char **buf) +{ + size_t n = 0; + off_t file_sz; + int code; + + if (io_debug) + fprintf(stderr, ">>> SIZE %s\n", fn); + + if (fprintf(fp, "SIZE %s\r\n", fn) < 0) + errx(1, "%s: fprintf", __func__); + + (void)fflush(fp); + if ((code = ftp_getline(buf, &n, 1, fp)) != P_OK) + return code; + + if (sscanf(*buf, "%*u %lld", &file_sz) != 1) + errx(1, "%s: sscanf size", __func__); + + if (file_sz < 0 || file_sz > INT64_MAX) + errx(1, "%s: size out of bounds: %lld", __func__, file_sz); + + if (sizep) + *sizep = file_sz; + + return code; +} + +int +ftp_eprt(FILE *fp) +{ + struct sockaddr_storage ss; + char addr[NI_MAXHOST], port[NI_MAXSERV], *eprt; + socklen_t len; + int e, on, ret, sock; + + len = sizeof(ss); + memset(&ss, 0, len); + if (getsockname(fileno(fp), (struct sockaddr *)&ss, &len) == -1) { + warn("%s: getsockname", __func__); + return -1; + } + + /* pick a free port */ + switch (ss.ss_family) { + case AF_INET: + ((struct sockaddr_in *)&ss)->sin_port = 0; + break; + case AF_INET6: + ((struct sockaddr_in6 *)&ss)->sin6_port = 0; + break; + default: + errx(1, "%s: Invalid socket family", __func__); + } + + if ((sock = socket(ss.ss_family, SOCK_STREAM, 0)) == -1) { + warn("%s: socket", __func__); + return -1; + } + + switch (ss.ss_family) { + case AF_INET: + on = IP_PORTRANGE_HIGH; + if (setsockopt(sock, IPPROTO_IP, IP_PORTRANGE, + (char *)&on, sizeof(on)) < 0) + warn("setsockopt IP_PORTRANGE (ignored)"); + break; + case AF_INET6: + on = IPV6_PORTRANGE_HIGH; + if (setsockopt(sock, IPPROTO_IPV6, IPV6_PORTRANGE, + (char *)&on, sizeof(on)) < 0) + warn("setsockopt IPV6_PORTRANGE (ignored)"); + break; + } + + if (bind(sock, (struct sockaddr *)&ss, len) == -1) { + close(sock); + warn("%s: bind", __func__); + return -1; + } + + if (listen(sock, 1) == -1) { + close(sock); + warn("%s: listen", __func__); + return -1; + } + + /* Find out the ephemeral port chosen */ + len = sizeof(ss); + memset(&ss, 0, len); + if (getsockname(sock, (struct sockaddr *)&ss, &len) == -1) { + close(sock); + warn("%s: getsockname", __func__); + return -1; + } + + if ((e = getnameinfo((struct sockaddr *)&ss, len, + addr, sizeof(addr), port, sizeof(port), + NI_NUMERICHOST | NI_NUMERICSERV)) != 0) { + close(sock); + warn("%s: getnameinfo: %s", __func__, gai_strerror(e)); + return -1; + } + + xasprintf(&eprt, "EPRT |%d|%s|%s|", + ss.ss_family == AF_INET ? 1 : 2, addr, port); + + ret = ftp_command(fp, "%s", eprt); + free(eprt); + if (ret != P_OK) { + close(sock); + return -1; + } + + return sock; +} + +int +ftp_epsv(FILE *fp) +{ + struct sockaddr_storage ss; + char *buf = NULL, delim[4], *s, *e; + size_t n = 0; + socklen_t len; + int error, port, sock; + + if (io_debug) + fprintf(stderr, ">>> EPSV\n"); + + if (fprintf(fp, "EPSV\r\n") < 0) + errx(1, "%s: fprintf", __func__); + + (void)fflush(fp); + if (ftp_getline(&buf, &n, 1, fp) != P_OK) { + free(buf); + return -1; + } + + if ((s = strchr(buf, '(')) == NULL || (e = strchr(s, ')')) == NULL) { + warnx("Malformed EPSV reply"); + free(buf); + return -1; + } + + s++; + *e = '\0'; + if (sscanf(s, "%c%c%c%d%c", &delim[0], &delim[1], &delim[2], + &port, &delim[3]) != 5) { + warnx("EPSV parse error"); + free(buf); + return -1; + } + free(buf); + + if (delim[0] != delim[1] || delim[0] != delim[2] + || delim[0] != delim[3]) { + warnx("EPSV parse error"); + return -1; + } + + len = sizeof(ss); + memset(&ss, 0, len); + if (getpeername(fileno(fp), (struct sockaddr *)&ss, &len) == -1) { + warn("%s: getpeername", __func__); + return -1; + } + + switch (ss.ss_family) { + case AF_INET: + ((struct sockaddr_in *)&ss)->sin_port = htons(port); + break; + case AF_INET6: + ((struct sockaddr_in6 *)&ss)->sin6_port = htons(port); + break; + default: + errx(1, "%s: Invalid socket family", __func__); + } + + if ((sock = socket(ss.ss_family, SOCK_STREAM, 0)) == -1) { + warn("%s: socket", __func__); + return -1; + } + + for (error = connect(sock, (struct sockaddr *)&ss, len); + error != 0 && errno == EINTR; error = connect_wait(sock)) + continue; + + if (error != 0) { + warn("%s: connect", __func__); + return -1; + } + + return sock; +} diff --git a/ftp.h b/ftp.h new file mode 100644 index 0000000..909465b --- /dev/null +++ b/ftp.h @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2015 Sunil Nimmagadda + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef FTP_H +#define FTP_H + +#include + +#include +#include + +#define S_HTTP 0 +#define S_FTP 1 +#define S_FILE 2 +#define S_HTTPS 3 + +#define TMPBUF_LEN 131072 + +#define P_PRE 100 +#define P_OK 200 +#define P_INTER 300 +#define N_TRANS 400 +#define N_PERM 500 + +#ifndef nitems +#define nitems(_a) (sizeof((_a)) / sizeof((_a)[0])) +#endif /* nitems */ + +struct url { + int scheme; + int ip_literal; + char *host; + char *port; + char *path; + char *basic_auth; +}; + +/* main.c */ +extern volatile sig_atomic_t interrupted; +extern struct url *ftp_proxy, *http_proxy; +extern const char *useragent; +extern char *oarg; +extern int activemode, family, io_debug, verbose, progressmeter; + +int fd_request(char *, int, off_t *); + +/* cmd.c */ +void cmd(const char *, const char *, const char *); + +/* file.c */ +struct url *file_get(struct url *, off_t *, off_t *); +void file_save(struct url *, FILE *, off_t *); + +/* ftp.c */ +void ftp_connect(struct url *, int); +struct url *ftp_get(struct url *, off_t *, off_t *); +void ftp_close(struct url *); +void ftp_save(struct url *, FILE *, off_t *); +int ftp_auth(FILE *, const char *, const char *); +int ftp_command(FILE *, const char *, ...) + __attribute__((__format__ (printf, 2, 3))) + __attribute__((__nonnull__ (2))); +int ftp_eprt(FILE *); +int ftp_epsv(FILE *); +int ftp_getline(char **, size_t *, int, FILE *); +int ftp_size(FILE *, const char *, off_t *, char **); + +/* http.c */ +void http_connect(struct url *, int); +struct url *http_get(struct url *, off_t *, off_t *); +void http_close(struct url *); +void http_save(struct url *, FILE *, off_t *); +void https_init(char *); + +/* progressmeter.c */ +void start_progress_meter(const char *, const char *, off_t, off_t *); +void stop_progress_meter(void); + +/* url.c */ +int url_scheme_lookup(const char *); +void url_connect(struct url *, int); +char *url_encode(const char *); +void url_free(struct url *); +struct url *xurl_parse(const char *); +struct url *url_parse(const char *); +struct url *url_request(struct url *, off_t *, off_t *); +void url_save(struct url *, FILE *, off_t *); +void url_close(struct url *); +char *url_str(struct url *); +const char *url_scheme_str(int); +const char *url_port_str(int); + +/* util.c */ +int connect_wait(int); +void copy_file(FILE *, FILE *, off_t *); +int tcp_connect(const char *, const char *, int); +void log_request(const char *, struct url *, struct url *); +void log_info(const char *, ...) + __attribute__((__format__ (printf, 1, 2))) + __attribute__((__nonnull__ (1))); + +#endif /* FTP_H */ diff --git a/http.c b/http.c new file mode 100644 index 0000000..9cbc59f --- /dev/null +++ b/http.c @@ -0,0 +1,833 @@ +/* + * Copyright (c) 2015 Sunil Nimmagadda + * Copyright (c) 2012 - 2015 Reyk Floeter + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifndef NOSSL +#include +#endif /* NOSSL */ + +#include "ftp.h" +#include "xmalloc.h" + +#define MAX_REDIRECTS 10 + +#ifndef NOSSL +#define MINBUF 128 + +static struct tls_config *tls_config; +static struct tls *ctx; +static int tls_session_fd = -1; +static char * const tls_verify_opts[] = { +#define HTTP_TLS_CAFILE 0 + "cafile", +#define HTTP_TLS_CAPATH 1 + "capath", +#define HTTP_TLS_CIPHERS 2 + "ciphers", +#define HTTP_TLS_DONTVERIFY 3 + "dont", +#define HTTP_TLS_VERIFYDEPTH 4 + "depth", +#define HTTP_TLS_MUSTSTAPLE 5 + "muststaple", +#define HTTP_TLS_NOVERIFYTIME 6 + "noverifytime", +#define HTTP_TLS_SESSION 7 + "session", +#define HTTP_TLS_DOVERIFY 8 + "do", + NULL +}; +#endif /* NOSSL */ + +/* + * HTTP status codes based on IANA assignments (2014-06-11 version): + * https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + * plus legacy (306) and non-standard (420). + */ +static struct http_status { + int code; + const char *name; +} http_status[] = { + { 100, "Continue" }, + { 101, "Switching Protocols" }, + { 102, "Processing" }, + /* 103-199 unassigned */ + { 200, "OK" }, + { 201, "Created" }, + { 202, "Accepted" }, + { 203, "Non-Authoritative Information" }, + { 204, "No Content" }, + { 205, "Reset Content" }, + { 206, "Partial Content" }, + { 207, "Multi-Status" }, + { 208, "Already Reported" }, + /* 209-225 unassigned */ + { 226, "IM Used" }, + /* 227-299 unassigned */ + { 300, "Multiple Choices" }, + { 301, "Moved Permanently" }, + { 302, "Found" }, + { 303, "See Other" }, + { 304, "Not Modified" }, + { 305, "Use Proxy" }, + { 306, "Switch Proxy" }, + { 307, "Temporary Redirect" }, + { 308, "Permanent Redirect" }, + /* 309-399 unassigned */ + { 400, "Bad Request" }, + { 401, "Unauthorized" }, + { 402, "Payment Required" }, + { 403, "Forbidden" }, + { 404, "Not Found" }, + { 405, "Method Not Allowed" }, + { 406, "Not Acceptable" }, + { 407, "Proxy Authentication Required" }, + { 408, "Request Timeout" }, + { 409, "Conflict" }, + { 410, "Gone" }, + { 411, "Length Required" }, + { 412, "Precondition Failed" }, + { 413, "Payload Too Large" }, + { 414, "URI Too Long" }, + { 415, "Unsupported Media Type" }, + { 416, "Range Not Satisfiable" }, + { 417, "Expectation Failed" }, + { 418, "I'm a teapot" }, + /* 419-421 unassigned */ + { 420, "Enhance Your Calm" }, + { 422, "Unprocessable Entity" }, + { 423, "Locked" }, + { 424, "Failed Dependency" }, + /* 425 unassigned */ + { 426, "Upgrade Required" }, + /* 427 unassigned */ + { 428, "Precondition Required" }, + { 429, "Too Many Requests" }, + /* 430 unassigned */ + { 431, "Request Header Fields Too Large" }, + /* 432-450 unassigned */ + { 451, "Unavailable For Legal Reasons" }, + /* 452-499 unassigned */ + { 500, "Internal Server Error" }, + { 501, "Not Implemented" }, + { 502, "Bad Gateway" }, + { 503, "Service Unavailable" }, + { 504, "Gateway Timeout" }, + { 505, "HTTP Version Not Supported" }, + { 506, "Variant Also Negotiates" }, + { 507, "Insufficient Storage" }, + { 508, "Loop Detected" }, + /* 509 unassigned */ + { 510, "Not Extended" }, + { 511, "Network Authentication Required" }, + /* 512-599 unassigned */ + { 0, NULL }, +}; + +struct http_headers { + char *location; + off_t content_length; + int chunked; + int retry_after; +}; + +static void decode_chunk(int, uint, FILE *); +static char *header_lookup(const char *, const char *); +static const char *http_error(int); +static int http_status_cmp(const void *, const void *); +static void http_headers_free(struct http_headers *); +static ssize_t http_getline(int, char **, size_t *); +static void http_proxy_connect(struct url *, struct url *); +static char *http_prepare_request(struct url *, off_t *); +static size_t http_read(int, char *, size_t); +static struct url *http_redirect(struct url *, char *); +static void http_copy_chunks(struct url *, FILE *, off_t *); +static int http_request(int, const char *, + struct http_headers **); +static char *relative_path_resolve(const char *, const char *); + +#ifndef NOSSL +static void tls_copy_file(struct url *, FILE *, off_t *); +static ssize_t tls_getline(char **, size_t *, struct tls *); +#endif /* NOSSL */ + +static FILE *fp; +static int chunked; + +void +http_connect(struct url *url, int timeout) +{ + static struct url *proxy; + const char *host, *port; + int sock; + + proxy = (url->scheme == S_HTTPS || url->scheme == S_HTTP) ? + http_proxy : ftp_proxy; + + host = proxy ? proxy->host : url->host; + port = proxy ? proxy->port : url->port; + if ((sock = tcp_connect(host, port, timeout)) == -1) + exit(1); + + if ((fp = fdopen(sock, "r+")) == NULL) + err(1, "%s: fdopen", __func__); + + if (proxy) + http_proxy_connect(proxy, url); + +#ifndef NOSSL + if (url->scheme != S_HTTPS) + return; + + if ((ctx = tls_client()) == NULL) + errx(1, "failed to create tls client"); + + if (tls_configure(ctx, tls_config) != 0) + errx(1, "%s: %s", __func__, tls_error(ctx)); + + if (tls_connect_socket(ctx, fileno(fp), url->host) != 0) + errx(1, "%s: %s", __func__, tls_error(ctx)); +#endif /* NOSSL */ +} + +static void +http_proxy_connect(struct url *proxy, struct url *url) +{ + struct http_headers *headers; + char *auth = NULL, *req; + int authlen = 0, code; + + if (proxy->basic_auth) { + authlen = xasprintf(&auth, + "Proxy-Authorization: Basic %s\r\n", proxy->basic_auth); + } + + xasprintf(&req, + "CONNECT %s:%s HTTP/1.0\r\n" + "User-Agent: %s\r\n" + "%s" + "\r\n", + url->host, url->port, + useragent, + proxy->basic_auth ? auth : ""); + + freezero(auth, authlen); + if ((code = http_request(S_HTTP, req, &headers)) != 200) + errx(1, "%s: Failed to CONNECT to %s:%s: %d %s", + __func__, url->host, url->port, code, http_error(code)); + + free(req); + http_headers_free(headers); +} + +static char * +http_prepare_request(struct url *url, off_t *offset) +{ + char *auth = NULL, *path = NULL, *range = NULL, *req; + int authlen = 0; + + if (*offset) + xasprintf(&range, "Range: bytes=%lld-\r\n", *offset); + + if (url->basic_auth) { + authlen = xasprintf(&auth, + "Authorization: Basic %s\r\n", url->basic_auth); + } + + if (url->path) + path = url_encode(url->path); + + xasprintf(&req, + "GET %s HTTP/1.1\r\n" + "Host: %s\r\n" + "%s" + "%s" + "Connection: close\r\n" + "User-Agent: %s\r\n" + "\r\n", + path ? path : "/", + url->host, + *offset ? range : "", + url->basic_auth ? auth : "", + useragent); + + free(range); + freezero(auth, authlen); + free(path); + return req; +} + +struct url * +http_get(struct url *url, off_t *offset, off_t *sz) +{ + struct http_headers *headers; + char *req; + int code, redirects = 0, retry = 0; + + do { + log_request("Requesting", url, http_proxy); + req = http_prepare_request(url, offset); + code = http_request(url->scheme, req, &headers); + free(req); + switch (code) { + case 200: + if (*offset) { + warnx("Server does not support resume."); + *offset = 0; + } + break; + case 206: + break; + case 301: + case 302: + case 303: + case 307: + http_close(url); + if (++redirects > MAX_REDIRECTS) + errx(1, "Too many redirections requested."); + + if (headers->location == NULL) { + errx(1, + "%s: Location header missing", __func__); + } + + url = http_redirect(url, headers->location); + http_headers_free(headers); + log_request("Redirected to", url, http_proxy); + http_connect(url, 0); + break; + case 416: + errx(1, "File is already fully retrieved."); + break; + case 503: + if (headers->retry_after == 0 && retry == 0) { + http_close(url); + http_headers_free(headers); + retry = 1; + log_request("Retrying", url, http_proxy); + http_connect(url, 0); + break; + } + /* FALLTHROUGH */ + default: + errx(1, "Error retrieving file: %d %s", + code, http_error(code)); + } + } while (code == 301 || code == 302 || + code == 303 || code == 307 || code == 503); + + *sz = headers->content_length + *offset; + chunked = headers->chunked; + http_headers_free(headers); + return url; +} + +void +http_save(struct url *url, FILE *dst_fp, off_t *offset) +{ + if (chunked) + http_copy_chunks(url, dst_fp, offset); +#ifndef NOSSL + else if (url->scheme == S_HTTPS) + tls_copy_file(url, dst_fp, offset); +#endif /* NOSSL */ + else + copy_file(dst_fp, fp, offset); +} + +static struct url * +http_redirect(struct url *old_url, char *location) +{ + struct url *new_url; + + /* absolute uri reference */ + if (strncasecmp(location, "http", 4) == 0 || + strncasecmp(location, "https", 5) == 0) { + new_url = xurl_parse(location); + goto done; + } + + /* relative uri reference */ + new_url = xcalloc(1, sizeof *new_url); + new_url->scheme = old_url->scheme; + new_url->host = xstrdup(old_url->host); + new_url->port = xstrdup(old_url->port); + + /* absolute-path reference */ + if (location[0] == '/') + new_url->path = xstrdup(location); + else + new_url->path = relative_path_resolve(old_url->path, location); + + done: + url_free(old_url); + return new_url; +} + +static char * +relative_path_resolve(const char *base_path, const char *location) +{ + char *new_path, *p; + + /* trim fragment component from both uri */ + if ((p = strchr(location, '#')) != NULL) + *p = '\0'; + if (base_path && (p = strchr(base_path, '#')) != NULL) + *p = '\0'; + + if (base_path == NULL) + xasprintf(&new_path, "/%s", location); + else if (base_path[strlen(base_path) - 1] == '/') + xasprintf(&new_path, "%s%s", base_path, location); + else { + p = dirname(base_path); + xasprintf(&new_path, "%s/%s", + strcmp(p, ".") == 0 ? "" : p, location); + } + + return new_path; +} + +static void +http_copy_chunks(struct url *url, FILE *dst_fp, off_t *offset) +{ + char *buf = NULL; + size_t n = 0; + uint chunk_sz; + + http_getline(url->scheme, &buf, &n); + if (sscanf(buf, "%x", &chunk_sz) != 1) + errx(1, "%s: Failed to get chunk size", __func__); + + while (chunk_sz > 0) { + decode_chunk(url->scheme, chunk_sz, dst_fp); + *offset += chunk_sz; + http_getline(url->scheme, &buf, &n); + if (sscanf(buf, "%x", &chunk_sz) != 1) + errx(1, "%s: Failed to get chunk size", __func__); + } + + free(buf); +} + +static void +decode_chunk(int scheme, uint sz, FILE *dst_fp) +{ + size_t bufsz; + size_t r; + char buf[BUFSIZ], crlf[2]; + + bufsz = sizeof(buf); + while (sz > 0) { + if (sz < bufsz) + bufsz = sz; + + r = http_read(scheme, buf, bufsz); + if (fwrite(buf, 1, r, dst_fp) != r) + errx(1, "%s: fwrite", __func__); + + sz -= r; + } + + /* CRLF terminating the chunk */ + if (http_read(scheme, crlf, sizeof(crlf)) != sizeof(crlf)) + errx(1, "%s: Failed to read terminal crlf", __func__); + + if (crlf[0] != '\r' || crlf[1] != '\n') + errx(1, "%s: Invalid chunked encoding", __func__); +} + +void +http_close(struct url *url) +{ +#ifndef NOSSL + ssize_t r; + + if (url->scheme == S_HTTPS) { + if (tls_session_fd != -1) + dprintf(STDERR_FILENO, "tls session resumed: %s\n", + tls_conn_session_resumed(ctx) ? "yes" : "no"); + + do { + r = tls_close(ctx); + } while (r == TLS_WANT_POLLIN || r == TLS_WANT_POLLOUT); + tls_free(ctx); + } + +#endif /* NOSSL */ + fclose(fp); + chunked = 0; +} + +static int +http_request(int scheme, const char *req, struct http_headers **hdrs) +{ + struct http_headers *headers; + const char *e; + char *buf = NULL, *p; + size_t n = 0; + ssize_t buflen; + uint code; +#ifndef NOSSL + size_t len; + ssize_t nw; +#endif /* NOSSL */ + + if (io_debug) + fprintf(stderr, "<<< %s", req); + + switch (scheme) { +#ifndef NOSSL + case S_HTTPS: + len = strlen(req); + while (len > 0) { + nw = tls_write(ctx, req, len); + if (nw == TLS_WANT_POLLIN || nw == TLS_WANT_POLLOUT) + continue; + if (nw < 0) + errx(1, "tls_write: %s", tls_error(ctx)); + req += nw; + len -= nw; + } + break; +#endif /* NOSSL */ + case S_HTTP: + if (fprintf(fp, "%s", req) < 0) + errx(1, "%s: fprintf", __func__); + (void)fflush(fp); + break; + } + + http_getline(scheme, &buf, &n); + if (io_debug) + fprintf(stderr, ">>> %s", buf); + + if (sscanf(buf, "%*s %u %*s", &code) != 1) + errx(1, "%s: failed to extract status code", __func__); + + if (code < 100 || code > 511) + errx(1, "%s: invalid status code %d", __func__, code); + + headers = xcalloc(1, sizeof *headers); + headers->retry_after = -1; + for (;;) { + buflen = http_getline(scheme, &buf, &n); + buflen -= 1; + if (buflen > 0 && buf[buflen - 1] == '\r') + buflen -= 1; + buf[buflen] = '\0'; + + if (io_debug) + fprintf(stderr, ">>> %s\n", buf); + + if (buflen == 0) + break; /* end of headers */ + + if ((p = header_lookup(buf, "Content-Length:")) != NULL) { + headers->content_length = strtonum(p, 0, INT64_MAX, &e); + if (e) + err(1, "%s: Content-Length is %s: %lld", + __func__, e, headers->content_length); + } + + if ((p = header_lookup(buf, "Location:")) != NULL) + headers->location = xstrdup(p); + + if ((p = header_lookup(buf, "Transfer-Encoding:")) != NULL) + if (strcasestr(p, "chunked") != NULL) + headers->chunked = 1; + + if ((p = header_lookup(buf, "Retry-After:")) != NULL) { + headers->retry_after = strtonum(p, 0, 0, &e); + if (e) + headers->retry_after = -1; + } + } + + *hdrs = headers; + free(buf); + return code; +} + +static void +http_headers_free(struct http_headers *headers) +{ + if (headers == NULL) + return; + + free(headers->location); + free(headers); +} + +static char * +header_lookup(const char *buf, const char *key) +{ + char *p; + + if (strncasecmp(buf, key, strlen(key)) == 0) { + if ((p = strchr(buf, ' ')) == NULL) + errx(1, "Failed to parse %s", key); + return ++p; + } + + return NULL; +} + +static ssize_t +http_getline(int scheme, char **buf, size_t *n) +{ + ssize_t buflen; + + switch (scheme) { +#ifndef NOSSL + case S_HTTPS: + if ((buflen = tls_getline(buf, n, ctx)) == -1) + errx(1, "%s: tls_getline", __func__); + break; +#endif /* NOSSL */ + case S_HTTP: + if ((buflen = getline(buf, n, fp)) == -1) + err(1, "%s: getline", __func__); + break; + default: + errx(1, "%s: invalid scheme", __func__); + } + + return buflen; +} + +static size_t +http_read(int scheme, char *buf, size_t size) +{ + size_t r; +#ifndef NOSSL + ssize_t rs; +#endif /* NOSSL */ + + switch (scheme) { +#ifndef NOSSL + case S_HTTPS: + do { + rs = tls_read(ctx, buf, size); + } while (rs == TLS_WANT_POLLIN || rs == TLS_WANT_POLLOUT); + if (rs == -1) + errx(1, "%s: tls_read: %s", __func__, tls_error(ctx)); + r = rs; + break; +#endif /* NOSSL */ + case S_HTTP: + if ((r = fread(buf, 1, size, fp)) < size) + if (!feof(fp)) + errx(1, "%s: fread", __func__); + break; + default: + errx(1, "%s: invalid scheme", __func__); + } + + return r; +} + +static const char * +http_error(int code) +{ + struct http_status error, *res; + + /* Set up key */ + error.code = code; + + if ((res = bsearch(&error, http_status, + sizeof(http_status) / sizeof(http_status[0]) - 1, + sizeof(http_status[0]), http_status_cmp)) != NULL) + return (res->name); + + return (NULL); +} + +static int +http_status_cmp(const void *a, const void *b) +{ + const struct http_status *ea = a; + const struct http_status *eb = b; + + return (ea->code - eb->code); +} + +#ifndef NOSSL +void +https_init(char *tls_options) +{ + char *str; + int depth; + const char *ca_file, *errstr; + + if (tls_init() != 0) + errx(1, "tls_init failed"); + + if ((tls_config = tls_config_new()) == NULL) + errx(1, "tls_config_new failed"); + + if (tls_config_set_protocols(tls_config, TLS_PROTOCOLS_ALL) != 0) + errx(1, "tls set protocols failed: %s", + tls_config_error(tls_config)); + + if (tls_config_set_ciphers(tls_config, "legacy") != 0) + errx(1, "tls set ciphers failed: %s", + tls_config_error(tls_config)); + + ca_file = tls_default_ca_cert_file(); + while (tls_options && *tls_options) { + switch (getsubopt(&tls_options, tls_verify_opts, &str)) { + case HTTP_TLS_CAFILE: + if (str == NULL) + errx(1, "missing CA file"); + ca_file = str; + break; + case HTTP_TLS_CAPATH: + if (str == NULL) + errx(1, "missing ca path"); + if (tls_config_set_ca_path(tls_config, str) != 0) + errx(1, "tls ca path failed"); + break; + case HTTP_TLS_CIPHERS: + if (str == NULL) + errx(1, "missing cipher list"); + if (tls_config_set_ciphers(tls_config, str) != 0) + errx(1, "tls set ciphers failed"); + break; + case HTTP_TLS_DONTVERIFY: + tls_config_insecure_noverifycert(tls_config); + tls_config_insecure_noverifyname(tls_config); + break; + case HTTP_TLS_VERIFYDEPTH: + if (str == NULL) + errx(1, "missing depth"); + depth = strtonum(str, 0, INT_MAX, &errstr); + if (errstr) + errx(1, "Cert validation depth is %s", errstr); + tls_config_set_verify_depth(tls_config, depth); + break; + case HTTP_TLS_MUSTSTAPLE: + tls_config_ocsp_require_stapling(tls_config); + break; + case HTTP_TLS_NOVERIFYTIME: + tls_config_insecure_noverifytime(tls_config); + break; + case HTTP_TLS_SESSION: + if (str == NULL) + errx(1, "missing session file"); + tls_session_fd = open(str, O_RDWR|O_CREAT, 0600); + if (tls_session_fd == -1) + err(1, "failed to open or create session file " + "'%s'", str); + if (tls_config_set_session_fd(tls_config, + tls_session_fd) == -1) + errx(1, "failed to set session: %s", + tls_config_error(tls_config)); + break; + case HTTP_TLS_DOVERIFY: + /* For compatibility, we do verify by default */ + break; + default: + errx(1, "Unknown -S suboption `%s'", + suboptarg ? suboptarg : ""); + } + } + + if (tls_config_set_ca_file(tls_config, ca_file) == -1) + errx(1, "tls_config_set_ca_file failed"); +} + +static ssize_t +tls_getline(char **buf, size_t *buflen, struct tls *tls) +{ + char *newb; + size_t newlen, off; + int ret; + unsigned char c; + + if (buf == NULL || buflen == NULL) + return -1; + + /* If buf is NULL, we have to assume a size of zero */ + if (*buf == NULL) + *buflen = 0; + + off = 0; + do { + do { + ret = tls_read(tls, &c, 1); + } while (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT); + if (ret == -1) + return -1; + + /* Ensure we can handle it */ + if (off + 2 > SSIZE_MAX) + return -1; + + newlen = off + 2; /* reserve space for NUL terminator */ + if (newlen > *buflen) { + newlen = newlen < MINBUF ? MINBUF : *buflen * 2; + newb = recallocarray(*buf, *buflen, newlen, 1); + if (newb == NULL) + return -1; + + *buf = newb; + *buflen = newlen; + } + + *(*buf + off) = c; + off += 1; + } while (c != '\n'); + + *(*buf + off) = '\0'; + return off; +} + +static void +tls_copy_file(struct url *url, FILE *dst_fp, off_t *offset) +{ + char *tmp_buf; + ssize_t r; + + tmp_buf = xmalloc(TMPBUF_LEN); + for (;;) { + do { + r = tls_read(ctx, tmp_buf, TMPBUF_LEN); + } while (r == TLS_WANT_POLLIN || r == TLS_WANT_POLLOUT); + + if (r == -1) + errx(1, "%s: tls_read: %s", __func__, tls_error(ctx)); + else if (r == 0) + break; + + *offset += r; + if (fwrite(tmp_buf, 1, r, dst_fp) != (size_t)r) + err(1, "%s: fwrite", __func__); + } + free(tmp_buf); +} +#endif /* NOSSL */ diff --git a/main.c b/main.c new file mode 100644 index 0000000..a3d9f67 --- /dev/null +++ b/main.c @@ -0,0 +1,526 @@ +/* + * Copyright (c) 2015 Sunil Nimmagadda + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ftp.h" +#include "xmalloc.h" + +#define IMSG_OPEN 1 + +static int auto_fetch(int, char **); +static void child(int, int, char **); +static int parent(int, pid_t); +static struct url *proxy_parse(const char *); +static int stdout_copy(const char *); +static int append(const char *); +static int save(const char *); +static int slurp(struct url *, FILE *, off_t *, off_t); +static char *output_fname(struct url *, const char *); +static void re_exec(int, int, char **); +static __dead void usage(void); +static int read_message(struct imsgbuf *, struct imsg *); +static void send_message(struct imsgbuf *, int, uint32_t, void *, + size_t, int); + +struct url *ftp_proxy, *http_proxy; +const char *useragent = "OpenBSD ftp"; +char *oarg; +int activemode, family = AF_UNSPEC, io_debug; +int progressmeter, verbose = 1; +volatile sig_atomic_t interrupted = 0; + +static struct imsgbuf child_ibuf; +static const char *title; +static char *tls_options; +static int connect_timeout, resume; + +int +main(int argc, char **argv) +{ + const char *e; + char **save_argv, *term; + int ch, csock, dumb_terminal, rexec, save_argc; + + if (isatty(fileno(stdin)) != 1) + verbose = 0; + + io_debug = getenv("IO_DEBUG") != NULL; + term = getenv("TERM"); + dumb_terminal = (term == NULL || *term == '\0' || + !strcmp(term, "dumb") || !strcmp(term, "emacs") || + !strcmp(term, "su")); + if (isatty(STDOUT_FILENO) && isatty(STDERR_FILENO) && !dumb_terminal) + progressmeter = 1; + + csock = rexec = 0; + save_argc = argc; + save_argv = argv; + while ((ch = getopt(argc, argv, + "46AaCc:dD:Eegik:MmN:no:pP:r:S:s:tU:vVw:xz:")) != -1) { + switch (ch) { + case '4': + family = AF_INET; + break; + case '6': + family = AF_INET6; + break; + case 'A': + activemode = 1; + break; + case 'C': + resume = 1; + break; + case 'D': + title = optarg; + break; + case 'o': + oarg = optarg; + if (!strlen(oarg)) + oarg = NULL; + break; + case 'M': + progressmeter = 0; + break; + case 'm': + progressmeter = 1; + break; + case 'N': + setprogname(optarg); + break; + case 'S': + tls_options = optarg; + break; + case 'U': + useragent = optarg; + break; + case 'V': + verbose = 0; + break; + case 'v': + verbose = 1; + break; + case 'w': + connect_timeout = strtonum(optarg, 0, 200, &e); + if (e) + errx(1, "-w: %s", e); + break; + /* options for internal use only */ + case 'x': + rexec = 1; + break; + case 'z': + csock = strtonum(optarg, 3, getdtablesize() - 1, &e); + if (e) + errx(1, "-z: %s", e); + break; + /* Ignoring all remaining options */ + case 'a': + case 'c': + case 'd': + case 'E': + case 'e': + case 'g': + case 'i': + case 'k': + case 'n': + case 'P': + case 'p': + case 'r': + case 's': + case 't': + warnx("Ignoring getopt: %c", ch); + break; + default: + usage(); + } + } + argc -= optind; + argv += optind; + + if (rexec) + child(csock, argc, argv); + +#ifndef SMALL + struct url *url; + + switch (argc) { + case 0: + cmd(NULL, NULL, NULL); + return 0; + case 1: + case 2: + switch (url_scheme_lookup(argv[0])) { + case -1: + cmd(argv[0], argv[1], NULL); + return 0; + case S_FTP: + url = xurl_parse(argv[0]); + if (url->path && + url->path[strlen(url->path) - 1] != '/') + break; /* auto fetch */ + + cmd(url->host, url->port, url->path); + return 0; + } + break; + } +#else + if (argc == 0) + usage(); +#endif /* SMALL */ + + return auto_fetch(save_argc, save_argv); +} + +static int +auto_fetch(int sargc, char **sargv) +{ + pid_t pid; + int sp[2]; + + if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, sp) != 0) + err(1, "socketpair"); + + switch (pid = fork()) { + case -1: + err(1, "fork"); + case 0: + close(sp[0]); + re_exec(sp[1], sargc, sargv); + } + + close(sp[1]); + return parent(sp[0], pid); +} + +static void +re_exec(int sock, int argc, char **argv) +{ + char **nargv, *sock_str; + int i, j, nargc; + + nargc = argc + 4; + nargv = xcalloc(nargc, sizeof(*nargv)); + xasprintf(&sock_str, "%d", sock); + i = 0; + nargv[i++] = argv[0]; + nargv[i++] = "-z"; + nargv[i++] = sock_str; + nargv[i++] = "-x"; + for (j = 1; j < argc; j++) + nargv[i++] = argv[j]; + + execvp(nargv[0], nargv); + err(1, "execvp"); +} + +static int +parent(int sock, pid_t child_pid) +{ + struct imsgbuf ibuf; + struct imsg imsg; + struct stat sb; + off_t offset; + int fd, save_errno, sig, status; + + setproctitle("%s", "parent"); + if (pledge("stdio cpath rpath wpath sendfd", NULL) == -1) + err(1, "pledge"); + + imsg_init(&ibuf, sock); + for (;;) { + if (read_message(&ibuf, &imsg) == 0) + break; + + if (imsg.hdr.type != IMSG_OPEN) + errx(1, "%s: IMSG_OPEN expected", __func__); + + offset = 0; + fd = open(imsg.data, imsg.hdr.peerid, 0666); + save_errno = errno; + if (fd != -1 && fstat(fd, &sb) == 0) { + if (sb.st_mode & S_IFDIR) { + close(fd); + fd = -1; + save_errno = EISDIR; + } else + offset = sb.st_size; + } + + send_message(&ibuf, IMSG_OPEN, save_errno, + &offset, sizeof offset, fd); + imsg_free(&imsg); + } + + close(sock); + if (waitpid(child_pid, &status, 0) == -1 && errno != ECHILD) + err(1, "wait"); + + sig = WTERMSIG(status); + if (WIFSIGNALED(status) && sig != SIGPIPE) + errx(1, "child terminated: signal %d", sig); + + return WEXITSTATUS(status); +} + +static void +child(int sock, int argc, char **argv) +{ + int i, to_stdout = 0, r = 0; + + setproctitle("%s", "child"); + +#ifndef NOSSL + /* + * TLS can't be init-ed on first use as filesystem(ca file) isn't + * available after pledge(2). + */ + https_init(tls_options); +#endif /* NOSSL */ + + if (pledge("stdio inet dns recvfd tty unveil", NULL) == -1) + err(1, "pledge"); + if (!progressmeter && + pledge("stdio inet dns recvfd unveil", NULL) == -1) + err(1, "pledge"); + + imsg_init(&child_ibuf, sock); + ftp_proxy = proxy_parse("ftp_proxy"); + http_proxy = proxy_parse("http_proxy"); + + if (oarg) { + if (strcmp(oarg, "-") == 0) { + to_stdout = 1; + if (resume) + errx(1, "can't append to stdout"); + } else if (unveil(oarg, "w") == -1) + err(1, "unveil"); + + if (unveil(NULL, NULL) == -1) + err(1, "unveil"); + } + + for (i = 0; i < argc; i++) { + if (to_stdout) + r = stdout_copy(argv[i]); + else if (resume) + r = append(argv[i]); + else + r = save(argv[i]); + } + + exit(r); +} + +static int +stdout_copy(const char *arg) +{ + struct url *url; + off_t offset = 0, sz = 0; + + url = xurl_parse(arg); + url_connect(url, connect_timeout); + url = url_request(url, &offset, &sz); + return slurp(url, stdout, &offset, sz); +} + +static int +append(const char *arg) +{ + struct url *url; + FILE *fp; + char *fname; + off_t offset = 0, sz = 0; + int fd; + + url = xurl_parse(arg); + url_connect(url, connect_timeout); + fname = output_fname(url, arg); + fd = fd_request(fname, O_WRONLY|O_APPEND, &offset); + url = url_request(url, &offset, &sz); + /* If HTTP server doesn't support range requests, truncate. */ + if (fd != -1 && offset == 0) + if (ftruncate(fd, 0) != 0) + err(1, "ftruncate"); + + if (fd == -1 && + (fd = fd_request(fname, O_CREAT|O_TRUNC|O_WRONLY, NULL)) == -1) + err(1, "Can't open file %s", fname); + + if ((fp = fdopen(fd, "w")) == NULL) + err(1, "%s: fdopen", __func__); + + return slurp(url, fp, &offset, sz); +} + +static int +save(const char *arg) +{ + struct url *url; + FILE *fp; + char *fname; + off_t offset = 0, sz = 0; + int fd, r; + + url = xurl_parse(arg); + url_connect(url, connect_timeout); + url = url_request(url, &offset, &sz); + fname = output_fname(url, arg); + if ((fd = fd_request(fname, O_CREAT|O_TRUNC|O_WRONLY, NULL)) == -1) + err(1, "Can't open file %s", fname); + + if ((fp = fdopen(fd, "w")) == NULL) + err(1, "%s: fdopen", __func__); + + return slurp(url, fp, &offset, sz); +} + +static int +slurp(struct url *url, FILE *fp, off_t *offset, off_t sz) +{ + start_progress_meter(basename(url->path), title, sz, offset); + url_save(url, fp, offset); + stop_progress_meter(); + url_close(url); + url_free(url); + if (fp != stdout) + fclose(fp); + + if (sz != 0 && *offset != sz) { + log_info("Read short file\n"); + return 1; + } + + return 0; +} + +static char * +output_fname(struct url *url, const char *arg) +{ + char *fname; + + fname = oarg ? oarg : basename(url->path); + if (strcmp(fname, "/") == 0) + errx(1, "No filename after host (use -o): %s", arg); + + if (strcmp(fname, ".") == 0) + errx(1, "No '/' after host (use -o): %s", arg); + + return fname; +} + +int +fd_request(char *path, int flags, off_t *offset) +{ + struct imsg imsg; + off_t *poffset; + int fd, save_errno; + + send_message(&child_ibuf, IMSG_OPEN, flags, path, strlen(path) + 1, -1); + if (read_message(&child_ibuf, &imsg) == 0) + return -1; + + if (imsg.hdr.type != IMSG_OPEN) + errx(1, "%s: IMSG_OPEN expected", __func__); + + fd = imsg.fd; + if (offset) { + poffset = imsg.data; + *offset = *poffset; + } + + save_errno = imsg.hdr.peerid; + imsg_free(&imsg); + errno = save_errno; + return fd; +} + +void +send_message(struct imsgbuf *ibuf, int type, uint32_t peerid, + void *msg, size_t msglen, int fd) +{ + if (imsg_compose(ibuf, type, peerid, 0, fd, msg, msglen) != 1) + err(1, "imsg_compose"); + + if (imsg_flush(ibuf) != 0) + err(1, "imsg_flush"); +} + +int +read_message(struct imsgbuf *ibuf, struct imsg *imsg) +{ + int n; + + if ((n = imsg_read(ibuf)) == -1) + err(1, "%s: imsg_read", __func__); + if (n == 0) + return 0; + + if ((n = imsg_get(ibuf, imsg)) == -1) + err(1, "%s: imsg_get", __func__); + if (n == 0) + return 0; + + return n; +} + +static struct url * +proxy_parse(const char *name) +{ + struct url *proxy; + char *str; + + if ((str = getenv(name)) == NULL) + return NULL; + + if (strlen(str) == 0) + return NULL; + + proxy = xurl_parse(str); + if (proxy->scheme != S_HTTP) + errx(1, "Malformed proxy URL: %s", str); + + return proxy; +} + +static __dead void +usage(void) +{ + fprintf(stderr, + "usage:\t%s [-46AVv] [-D title] [host [port]]\n" + "\t%s [-46ACVMmVv] [-N name] [-D title] [-o output]\n" + "\t\t [-S tls_options] [-U useragent] [-w seconds] url ...\n", + getprogname(), getprogname()); + + exit(1); +} diff --git a/progressmeter.c b/progressmeter.c new file mode 100644 index 0000000..ec9b8ee --- /dev/null +++ b/progressmeter.c @@ -0,0 +1,358 @@ +/* + * Copyright (c) 2015 Sunil Nimmagadda + * Copyright (c) 2003 Nils Nordman. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "ftp.h" + +#define DEFAULT_WINSIZE 80 +#define MAX_WINSIZE 512 +#define UPDATE_INTERVAL 1 /* update the progress meter every second */ +#define STALL_TIME 5 /* we're stalled after this many seconds */ + +time_t monotime(void); + +/* formats and inserts the specified size into the given buffer */ +static void format_size(char *, int, off_t); +static void format_rate(char *, int, off_t); + +/* window resizing */ +static void sig_winch(int); +static void setscreensize(void); + +/* updates the progressmeter to reflect the current state of the transfer */ +void refresh_progress_meter(void); + +/* signal handler for updating the progress meter */ +static void update_progress_meter(int); + +static const char *title; /* short title for the start of progress bar */ +static time_t start; /* start progress */ +static time_t last_update; /* last progress update */ +static off_t start_pos; /* initial position of transfer */ +static off_t end_pos; /* ending position of transfer */ +static off_t cur_pos; /* transfer position as of last refresh */ +static off_t offset; /* initial offset from start_pos */ +static volatile off_t *counter; /* progress counter */ +static long stalled; /* how long we have been stalled */ +static int bytes_per_second; /* current speed in bytes per second */ +static int win_size; /* terminal window size */ +static volatile sig_atomic_t win_resized; /* for window resizing */ +static const char *filename; /* To be displayed in non-verbose mode */ +/* units for format_size */ +static const char unit[] = " KMGT"; + +time_t +monotime(void) +{ + struct timespec ts; + + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) + err(1, "monotime"); + + return ts.tv_sec; +} + +static void +format_rate(char *buf, int size, off_t bytes) +{ + int i; + + bytes *= 100; + for (i = 0; bytes >= 100*1000 && unit[i] != 'T'; i++) + bytes = (bytes + 512) / 1024; + if (i == 0) { + i++; + bytes = (bytes + 512) / 1024; + } + snprintf(buf, size, "%lld.%02lld %c%s", + (long long) (bytes + 5) / 100, + (long long) (bytes + 5) / 10 % 10, + unit[i], + "B"); +} + +static void +format_size(char *buf, int size, off_t bytes) +{ + int i; + + for (i = 0; bytes >= 10000 && unit[i] != 'T'; i++) + bytes = (bytes + 512) / 1024; + snprintf(buf, size, "%4lld%c%s", + (long long) bytes, + unit[i], + i ? "B" : " "); +} + +void +refresh_progress_meter(void) +{ + char buf[MAX_WINSIZE + 1]; + const char *dot = ""; + time_t now; + off_t transferred, bytes_left; + double elapsed; + int len, cur_speed, hours, minutes, seconds, barlength, i; + int percent, overhead = 30; + + transferred = *counter - (cur_pos ? cur_pos : start_pos); + cur_pos = *counter; + now = monotime(); + bytes_left = end_pos - cur_pos; + + if (bytes_left > 0) + elapsed = now - last_update; + else { + elapsed = now - start; + /* Calculate true total speed when done */ + transferred = end_pos - start_pos; + bytes_per_second = 0; + } + + /* calculate speed */ + if (elapsed != 0) + cur_speed = (transferred / elapsed); + else + cur_speed = transferred; + +#define AGE_FACTOR 0.9 + if (bytes_per_second != 0) { + bytes_per_second = (bytes_per_second * AGE_FACTOR) + + (cur_speed * (1.0 - AGE_FACTOR)); + } else + bytes_per_second = cur_speed; + + buf[0] = '\0'; + /* title */ + if (!verbose && title != NULL) { + len = strlen(title); + if (len < 7) + len = 7; + else if (len > 12) { + len = 12; + dot = "..."; + overhead += 3; + } + snprintf(buf, sizeof buf, "\r%-*.*s%s ", len, len, title, dot); + overhead += len + 1; + } else + snprintf(buf, sizeof buf, "\r"); + + if (end_pos == 0 || cur_pos == end_pos) + percent = 100; + else + percent = ((float)cur_pos / end_pos) * 100; + + /* filename and percent */ + if (!verbose && filename != NULL) { + len = strlen(filename); + if (len < 12) + len = 12; + else if (len > 25) { + len = 22; + dot = "..."; + overhead += 3; + } + snprintf(buf + strlen(buf), sizeof buf - strlen(buf), + "%-*.*s%s %3d%% ", len, len, filename, dot, percent); + overhead += len + 1; + } else + snprintf(buf, sizeof buf, "\r%3d%% ", percent); + + /* bar */ + barlength = win_size - overhead; + if (barlength > 0) { + i = barlength * percent / 100; + snprintf(buf + strlen(buf), sizeof buf - strlen(buf), + "|%.*s%*s| ", i, + "*******************************************************" + "*******************************************************" + "*******************************************************" + "*******************************************************" + "*******************************************************" + "*******************************************************" + "*******************************************************", + barlength - i, ""); + + } + + /* amount transferred */ + format_size(buf + strlen(buf), win_size - strlen(buf), cur_pos); + strlcat(buf, " ", win_size); + + /* ETA */ + if (!transferred) + stalled += elapsed; + else + stalled = 0; + + if (stalled >= STALL_TIME) + strlcat(buf, "- stalled -", win_size); + else if (bytes_per_second == 0 && bytes_left) + strlcat(buf, " --:-- ETA", win_size); + else { + if (bytes_left > 0) + seconds = bytes_left / bytes_per_second; + else + seconds = elapsed; + + hours = seconds / 3600; + seconds -= hours * 3600; + minutes = seconds / 60; + seconds -= minutes * 60; + + if (hours != 0) + snprintf(buf + strlen(buf), win_size - strlen(buf), + "%d:%02d:%02d", hours, minutes, seconds); + else + snprintf(buf + strlen(buf), win_size - strlen(buf), + " %02d:%02d", minutes, seconds); + + if (bytes_left > 0) + strlcat(buf, " ETA", win_size); + else + strlcat(buf, " ", win_size); + } + + if (progressmeter) + write(STDERR_FILENO, buf, strlen(buf)); + + last_update = now; +} + +static void +update_progress_meter(int ignore) +{ + int save_errno; + + save_errno = errno; + + if (win_resized) { + setscreensize(); + win_resized = 0; + } + + refresh_progress_meter(); + + signal(SIGALRM, update_progress_meter); + alarm(UPDATE_INTERVAL); + errno = save_errno; +} + +void +start_progress_meter(const char *fn, const char *t, off_t filesize, off_t *ctr) +{ + start = last_update = monotime(); + start_pos = *ctr; + offset = *ctr; + cur_pos = 0; + end_pos = 0; + counter = ctr; + stalled = 0; + bytes_per_second = 0; + filename = fn; + title = t; + + /* + * Suppress progressmeter if filesize isn't known when + * Content-Length header has bogus values. + */ + if (filesize <= 0) + return; + + end_pos = filesize; + if (progressmeter) + setscreensize(); + + refresh_progress_meter(); + + signal(SIGALRM, update_progress_meter); + signal(SIGWINCH, sig_winch); + alarm(UPDATE_INTERVAL); +} + +void +stop_progress_meter(void) +{ + char rate_str[32]; + double elapsed; + + alarm(0); + + /* Ensure we complete the progress */ + if (end_pos && cur_pos != end_pos) + refresh_progress_meter(); + + if (progressmeter && end_pos) + write(STDERR_FILENO, "\n", 1); + + if (!verbose) + return; + + elapsed = monotime() - start; + if (end_pos == 0) { + if (elapsed != 0) + bytes_per_second = *counter / elapsed; + else + bytes_per_second = *counter; + } + + format_rate(rate_str, sizeof rate_str, bytes_per_second); + log_info("%lld byte%s received in %.2f seconds (%s/s)\n", + (end_pos) ? cur_pos - offset : *counter, + *counter != 1 ? "s" : "", elapsed, rate_str); +} + +static void +sig_winch(int sig) +{ + win_resized = 1; +} + +static void +setscreensize(void) +{ + struct winsize winsize; + + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &winsize) != -1 && + winsize.ws_col != 0) { + if (winsize.ws_col > MAX_WINSIZE) + win_size = MAX_WINSIZE; + else + win_size = winsize.ws_col; + } else + win_size = DEFAULT_WINSIZE; + win_size += 1; /* trailing \0 */ +} diff --git a/regress/Makefile b/regress/Makefile new file mode 100644 index 0000000..5301289 --- /dev/null +++ b/regress/Makefile @@ -0,0 +1,3 @@ +SUBDIR+= unit-tests + +.include diff --git a/regress/unit-tests/Makefile b/regress/unit-tests/Makefile new file mode 100644 index 0000000..e67c6f6 --- /dev/null +++ b/regress/unit-tests/Makefile @@ -0,0 +1,2 @@ +SUBDIR+= url_parse +. include diff --git a/regress/unit-tests/url_parse/Makefile b/regress/unit-tests/url_parse/Makefile new file mode 100644 index 0000000..608eb81 --- /dev/null +++ b/regress/unit-tests/url_parse/Makefile @@ -0,0 +1,17 @@ +FTPREL= ../../../ +.PATH: ${.CURDIR}/${FTPREL} + +PROG=test_url_parse +SRCS=test_url_parse.c +SRCS+=file.c ftp.c http.c progressmeter.c url.c util.c xmalloc.c + +CFLAGS+=-I ${.CURDIR}/${FTPREL} +LDADD+= -lutil -ltls -lssl -lcrypto +DPADD+= ${LIBUTIL} ${LIBTLS} ${LIBSSL} ${LIBCRYPTO} + +REGRESS_TARGETS=run-regress-${PROG} + +run-regress-${PROG}: ${PROG} + env ${TEST_ENV} ./${PROG} + +.include diff --git a/regress/unit-tests/url_parse/test_url_parse.c b/regress/unit-tests/url_parse/test_url_parse.c new file mode 100644 index 0000000..5c15d35 --- /dev/null +++ b/regress/unit-tests/url_parse/test_url_parse.c @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2020 Sunil Nimmagadda + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include + +#include "ftp.h" + +struct url *ftp_proxy, *http_proxy; +volatile sig_atomic_t interrupted; +const char *useragent; +char *oarg; +int activemode, family, io_debug, verbose, progressmeter; + +int +fd_request(char *path, int flags, off_t *offset) +{ + /* dummy */ + return 0; +} + +static struct { + const char *str; + struct url url; + int noparse; +} testcases[] = { + { "http://google.com/index.html", { + S_HTTP, 0, "google.com", "80", "/index.html" } }, + { "https://google.com:", { + S_HTTPS, 0, "google.com", "443" } }, + { "file:.", { + S_FILE, 0, NULL, NULL, "." } }, + { "http://[::1]:/index.html", { + S_HTTP, 1, "::1", "80", "/index.html" } }, + { "http://[::1]:1234/", { + S_HTTP, 1, "::1", "1234", "/" } }, + { "foo.bar", {}, 1 }, + { "http://[::1:1234", {}, 1 }, + { "http://[1::2::3]:1234", { + S_HTTP, 0, "1::2::3", "1234" } }, + { "http://foo.com:bar", { + S_HTTP, 0, "foo.com", "bar" } }, + { "http:/foo.com", {}, 1 }, + { "http://foo:bar@baz.com", { + S_HTTP, 0, "baz.com", "80" } }, + { "http://[::1]abcd/", {}, 1 }, + { " http://localhost:8080", { + S_HTTP, 0, "localhost", "8080" } }, + { "ftps://localhost:21", {}, 1 }, + { "http://marc.info/?l=openbsd-tech&m=151790635206581&q=raw", { + S_HTTP, 0, "marc.info", "80", "/?l=openbsd-tech&m=151790635206581&q=raw" } }, + { "file://disklabel.template", { + S_FILE, 0, NULL, NULL, "/disklabel.template" } }, + { "file:/disklabel.template", { + S_FILE, 0, NULL, NULL, "/disklabel.template" } }, + { "file:///disklabel.template", { + S_FILE, 0, NULL, NULL, "/disklabel.template" } }, +}; + +static int +ptr_null_cmp(void *a, void *b) +{ + if ((a && b == NULL) || (a == NULL && b)) + return 1; + + return 0; +} + +static int +url_cmp(struct url *a, struct url *b) +{ + if (ptr_null_cmp(a, b) || + ptr_null_cmp(a->host, b->host) || + ptr_null_cmp(a->port, b->port) || + ptr_null_cmp(a->path, b->path)) + return 1; + + if (a->scheme != b->scheme || + (a->host && strcmp(a->host, b->host)) || + (a->port && strcmp(a->port, b->port)) || + (a->path && strcmp(a->path, b->path))) + return 1; + + return 0; +} + +int +main(void) +{ + struct url *url, *eurl; + size_t i; + + if (freopen("/dev/null", "w", stderr) == NULL) + err(1, "freopen"); + + for (i = 0; i < nitems(testcases); i++) { + url = url_parse(testcases[i].str); + if (testcases[i].noparse) { + if (url != NULL) + goto bad; + + continue; + } + + if (url_cmp(url, &testcases[i].url) != 0) + goto bad; + } + + return 0; + + bad: + printf("%s\n", testcases[i].str); + eurl = &testcases[i].url; + printf("Expected: scheme = %s, host = %s, port = %s, path = %s\n", + url_scheme_str(eurl->scheme), eurl->host, eurl->port, eurl->path); + printf("Got: scheme = %s, host = %s, port = %s, path = %s\n", + url_scheme_str(url->scheme), url->host, url->port, url->path); + return 1; +} diff --git a/url.c b/url.c new file mode 100644 index 0000000..546d448 --- /dev/null +++ b/url.c @@ -0,0 +1,424 @@ +/* + * Copyright (c) 2017 Sunil Nimmagadda + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/*- + * Copyright (c) 1997 The NetBSD Foundation, Inc. + * All rights reserved. + * + * This code is derived from software contributed to The NetBSD Foundation + * by Jason Thorpe and Luke Mewburn. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "ftp.h" +#include "xmalloc.h" + +#define BASICAUTH_LEN 1024 + +static void authority_parse(const char *, char **, char **, char **); +static int ipv6_parse(const char *, char **, char **); +static int unsafe_char(const char *); + +#ifndef NOSSL +const char *scheme_str[] = { "http:", "ftp:", "file:", "https:" }; +const char *port_str[] = { "80", "21", NULL, "443" }; +#else +const char *scheme_str[] = { "http:", "ftp:", "file:" }; +const char *port_str[] = { "80", "21", NULL }; +#endif /* NOSSL */ + +int +url_scheme_lookup(const char *str) +{ + size_t i; + +#ifdef NOSSL + if (strncasecmp(str, "https:", 6) == 0) + errx(1, "No HTTPS support."); +#endif /* NOSSL */ + + for (i = 0; i < nitems(scheme_str); i++) + if (strncasecmp(str, scheme_str[i], strlen(scheme_str[i])) == 0) + return i; + + return -1; +} + +static int +ipv6_parse(const char *str, char **host, char **port) +{ + char *p; + + if ((p = strchr(str, ']')) == NULL) { + warnx("%s: invalid IPv6 address: %s", __func__, str); + return 1; + } + + *p++ = '\0'; + if (strlen(str + 1) > 0) + *host = xstrdup(str + 1); + + if (*p == '\0') + return 0; + + if (*p++ != ':') { + warnx("%s: invalid port: %s", __func__, p); + free(*host); + return 1; + } + + if (strlen(p) > 0) + *port = xstrdup(p); + + return 0; +} + +static void +authority_parse(const char *str, char **host, char **port, char **basic_auth) +{ + char *p; + + if ((p = strchr(str, '@')) != NULL) { + *basic_auth = xcalloc(1, BASICAUTH_LEN); + if (b64_ntop((unsigned char *)str, p - str, + *basic_auth, BASICAUTH_LEN) == -1) + errx(1, "base64 encode failed"); + + str = ++p; + } + + if ((p = strchr(str, ':')) != NULL) { + *p++ = '\0'; + if (strlen(p) > 0) + *port = xstrdup(p); + } + + if (strlen(str) > 0) + *host = xstrdup(str); +} + +struct url * +xurl_parse(const char *str) +{ + struct url *url; + + if ((url = url_parse(str)) == NULL) + exit(1); + + return url; +} + +struct url * +url_parse(const char *str) +{ + struct url *url; + const char *p, *q; + char *basic_auth, *host, *port, *path, *s; + size_t len; + int ip_literal, scheme; + + p = str; + ip_literal = 0; + host = port = path = basic_auth = NULL; + while (isblank((unsigned char)*p)) + p++; + + if ((q = strchr(p, ':')) == NULL) { + warnx("%s: scheme missing: %s", __func__, str); + return NULL; + } + + if ((scheme = url_scheme_lookup(p)) == -1) { + warnx("%s: invalid scheme: %s", __func__, p); + return NULL; + } + + p = ++q; + if (strncmp(p, "//", 2) != 0) { + if (scheme == S_FILE) + goto done; + else { + warnx("%s: invalid url: %s", __func__, str); + return NULL; + } + } + + p += 2; + + /* + * quirk to parse file:// which isn't valid but required for + * backwards compatibility. + */ + if (scheme == S_FILE) { + q = (*p == '/') ? p : p - 1; + goto done; + } + + len = strlen(p); + /* Authority terminated by a '/' if present */ + if ((q = strchr(p, '/')) != NULL) + len = q - p; + + s = xstrndup(p, len); + if (*p == '[') { + if (ipv6_parse(s, &host, &port) != 0) { + free(s); + return NULL; + } + ip_literal = 1; + } else + authority_parse(s, &host, &port, &basic_auth); + + free(s); + if (port == NULL && scheme != S_FILE) + port = xstrdup(port_str[scheme]); + + done: + if (q != NULL) + path = xstrdup(q); + + if (io_debug) { + fprintf(stderr, + "scheme: %s\nhost: %s\nport: %s\npath: %s\n", + scheme_str[scheme], host, port, path); + } + + url = xcalloc(1, sizeof *url); + url->scheme = scheme; + url->host = host; + url->port = port; + url->path = path; + url->basic_auth = basic_auth; + url->ip_literal = ip_literal; + return url; +} + +void +url_free(struct url *url) +{ + if (url == NULL) + return; + + free(url->host); + free(url->port); + free(url->path); + freezero(url->basic_auth, BASICAUTH_LEN); + free(url); +} + +void +url_connect(struct url *url, int timeout) +{ + switch (url->scheme) { + case S_HTTP: + case S_HTTPS: + http_connect(url, timeout); + break; + case S_FTP: + if (ftp_proxy) + http_connect(url, timeout); + else + ftp_connect(url, timeout); + break; + } +} + +struct url * +url_request(struct url *url, off_t *offset, off_t *sz) +{ + switch (url->scheme) { + case S_HTTP: + case S_HTTPS: + return http_get(url, offset, sz); + case S_FTP: + if (ftp_proxy) + return http_get(url, offset, sz); + + return ftp_get(url, offset, sz); + case S_FILE: + return file_get(url, offset, sz); + } + + return NULL; +} + +void +url_save(struct url *url, FILE *dst_fp, off_t *offset) +{ + switch (url->scheme) { + case S_HTTP: + case S_HTTPS: + http_save(url, dst_fp, offset); + break; + case S_FTP: + if (ftp_proxy) + http_save(url, dst_fp, offset); + else + ftp_save(url, dst_fp, offset); + break; + case S_FILE: + file_save(url, dst_fp, offset); + break; + } +} + +void +url_close(struct url *url) +{ + switch (url->scheme) { + case S_HTTP: + case S_HTTPS: + http_close(url); + break; + case S_FTP: + if (ftp_proxy) + http_close(url); + else + ftp_close(url); + break; + } +} + +char * +url_str(struct url *url) +{ + char *host, *str; + int custom_port; + + custom_port = strcmp(url->port, port_str[url->scheme]) ? 1 : 0; + if (url->ip_literal) + xasprintf(&host, "[%s]", url->host); + else + host = xstrdup(url->host); + + xasprintf(&str, "%s//%s%s%s%s", + scheme_str[url->scheme], + host, + custom_port ? ":" : "", + custom_port ? url->port : "", + url->path ? url->path : "/"); + + free(host); + return str; +} + +const char * +url_scheme_str(int scheme) +{ + return scheme_str[scheme]; +} + +const char * +url_port_str(int scheme) +{ + return port_str[scheme]; +} + +/* + * Encode given URL, per RFC1738. + * Allocate and return string to the caller. + */ +char * +url_encode(const char *path) +{ + size_t i, length, new_length; + char *epath, *epathp; + + length = new_length = strlen(path); + + /* + * First pass: + * Count unsafe characters, and determine length of the + * final URL. + */ + for (i = 0; i < length; i++) + if (unsafe_char(path + i)) + new_length += 2; + + epath = epathp = xmalloc(new_length + 1); /* One more for '\0'. */ + + /* + * Second pass: + * Encode, and copy final URL. + */ + for (i = 0; i < length; i++) + if (unsafe_char(path + i)) { + snprintf(epathp, 4, "%%" "%02x", + (unsigned char)path[i]); + epathp += 3; + } else + *(epathp++) = path[i]; + + *epathp = '\0'; + return epath; +} + +/* + * Determine whether the character needs encoding, per RFC1738: + * - No corresponding graphic US-ASCII. + * - Unsafe characters. + */ +static int +unsafe_char(const char *c0) +{ + const char *unsafe_chars = " <>\"#{}|\\^~[]`"; + const unsigned char *c = (const unsigned char *)c0; + + /* + * No corresponding graphic US-ASCII. + * Control characters and octets not used in US-ASCII. + */ + return (iscntrl(*c) || !isascii(*c) || + + /* + * Unsafe characters. + * '%' is also unsafe, if is not followed by two + * hexadecimal digits. + */ + strchr(unsafe_chars, *c) != NULL || + (*c == '%' && (!isxdigit(*++c) || !isxdigit(*++c)))); +} diff --git a/util.c b/util.c new file mode 100644 index 0000000..463fb8b --- /dev/null +++ b/util.c @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2015 Sunil Nimmagadda + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ftp.h" +#include "xmalloc.h" + +static void tooslow(int); + +/* + * Wait for an asynchronous connect(2) attempt to finish. + */ +int +connect_wait(int s) +{ + struct pollfd pfd[1]; + int error = 0; + socklen_t len = sizeof(error); + + pfd[0].fd = s; + pfd[0].events = POLLOUT; + + if (poll(pfd, 1, -1) == -1) + return -1; + if (getsockopt(s, SOL_SOCKET, SO_ERROR, &error, &len) < 0) + return -1; + if (error != 0) { + errno = error; + return -1; + } + return 0; +} + +static void +tooslow(int signo) +{ + dprintf(STDERR_FILENO, "%s: connect taking too long\n", getprogname()); + _exit(2); +} + +int +tcp_connect(const char *host, const char *port, int timeout) +{ + struct addrinfo hints, *res, *res0; + char hbuf[NI_MAXHOST]; + const char *cause = NULL; + int error, s = -1, save_errno; + + if (host == NULL) { + warnx("hostname missing"); + return -1; + } + + memset(&hints, 0, sizeof hints); + hints.ai_family = family; + hints.ai_socktype = SOCK_STREAM; + if ((error = getaddrinfo(host, port, &hints, &res0))) { + warnx("%s: %s", host, gai_strerror(error)); + return -1; + } + + if (timeout) { + (void)signal(SIGALRM, tooslow); + alarm(timeout); + } + + for (res = res0; res; res = res->ai_next) { + if (getnameinfo(res->ai_addr, res->ai_addrlen, hbuf, + sizeof hbuf, NULL, 0, NI_NUMERICHOST) != 0) + (void)strlcpy(hbuf, "(unknown)", sizeof hbuf); + + log_info("Trying %s...\n", hbuf); + s = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + if (s == -1) { + cause = "socket"; + continue; + } + + for (error = connect(s, res->ai_addr, res->ai_addrlen); + error != 0 && errno == EINTR; error = connect_wait(s)) + continue; + + if (error != 0) { + cause = "connect"; + save_errno = errno; + close(s); + errno = save_errno; + s = -1; + continue; + } + + break; + } + + freeaddrinfo(res0); + if (s == -1) { + warn("%s", cause); + return -1; + } + + if (timeout) { + signal(SIGALRM, SIG_DFL); + alarm(0); + } + + return s; +} + +void +log_info(const char *fmt, ...) +{ + va_list ap; + + if (verbose == 0) + return; + + va_start(ap, fmt); + if (oarg && strcmp(oarg, "-") == 0) + vfprintf(stderr, fmt, ap); + else + vprintf(fmt, ap); + + va_end(ap); +} + +void +log_request(const char *prefix, struct url *url, struct url *proxy) +{ + char *host; + int custom_port; + + if (url->scheme == S_FILE) + return; + + custom_port = strcmp(url->port, url_port_str(url->scheme)) ? 1 : 0; + if (url->ip_literal) + xasprintf(&host, "[%s]", url->host); + else + host = xstrdup(url->host); + + if (proxy) + log_info("%s %s//%s%s%s%s" + " (via %s//%s%s%s)\n", + prefix, + url_scheme_str(url->scheme), + host, + custom_port ? ":" : "", + custom_port ? url->port : "", + url->path ? url->path : "", + + /* via proxy part */ + (proxy->scheme == S_HTTP) ? "http" : "https", + proxy->host, + proxy->port ? ":" : "", + proxy->port ? proxy->port : ""); + else + log_info("%s %s//%s%s%s%s\n", + prefix, + url_scheme_str(url->scheme), + host, + custom_port ? ":" : "", + custom_port ? url->port : "", + url->path ? url->path : ""); + + free(host); +} + +void +copy_file(FILE *dst, FILE *src, off_t *offset) +{ + char *tmp_buf; + size_t r; + + tmp_buf = xmalloc(TMPBUF_LEN); + while ((r = fread(tmp_buf, 1, TMPBUF_LEN, src)) != 0 && !interrupted) { + *offset += r; + if (fwrite(tmp_buf, 1, r, dst) != r) + err(1, "%s: fwrite", __func__); + } + + if (interrupted) { + free(tmp_buf); + return; + } + + if (!feof(src)) + errx(1, "%s: fread", __func__); + + free(tmp_buf); +} diff --git a/xmalloc.c b/xmalloc.c new file mode 100644 index 0000000..cd85939 --- /dev/null +++ b/xmalloc.c @@ -0,0 +1,147 @@ +/* $OpenBSD: xmalloc.c,v 1.11 2016/11/17 10:06:08 nicm Exp $ */ + +/* + * Author: Tatu Ylonen + * Copyright (c) 1995 Tatu Ylonen , Espoo, Finland + * All rights reserved + * Versions of malloc and friends that check their results, and never return + * failure (they call fatalx if they encounter an error). + * + * As far as I am concerned, the code I have written for this software + * can be used freely for any purpose. Any derived versions of this + * software must be clearly marked as such, and if the derived work is + * incompatible with the protocol description in the RFC file, it must be + * called by a name other than "ssh" or "Secure Shell". + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "xmalloc.h" + +void * +xmalloc(size_t size) +{ + void *ptr; + + if (size == 0) + errx(1, "xmalloc: zero size"); + ptr = malloc(size); + if (ptr == NULL) + err(1, "xmalloc: allocating %zu bytes", size); + return ptr; +} + +void * +xcalloc(size_t nmemb, size_t size) +{ + void *ptr; + + if (size == 0 || nmemb == 0) + errx(1, "xcalloc: zero size"); + ptr = calloc(nmemb, size); + if (ptr == NULL) + err(1, "xcalloc: allocating %zu * %zu bytes", nmemb, size); + return ptr; +} + +void * +xrealloc(void *ptr, size_t size) +{ + return xreallocarray(ptr, 1, size); +} + +void * +xreallocarray(void *ptr, size_t nmemb, size_t size) +{ + void *new_ptr; + + if (nmemb == 0 || size == 0) + errx(1, "xreallocarray: zero size"); + new_ptr = reallocarray(ptr, nmemb, size); + if (new_ptr == NULL) + err(1, "xreallocarray: allocating %zu * %zu bytes", + nmemb, size); + return new_ptr; +} + +char * +xstrdup(const char *str) +{ + char *cp; + + if ((cp = strdup(str)) == NULL) + err(1, "xstrdup"); + return cp; +} + +char * +xstrndup(const char *str, size_t maxlen) +{ + char *cp; + + if ((cp = strndup(str, maxlen)) == NULL) + err(1, "xstrndup"); + return cp; +} + +int +xasprintf(char **ret, const char *fmt, ...) +{ + va_list ap; + int i; + + va_start(ap, fmt); + i = xvasprintf(ret, fmt, ap); + va_end(ap); + + return i; +} + +int +xvasprintf(char **ret, const char *fmt, va_list ap) +{ + int i; + + i = vasprintf(ret, fmt, ap); + + if (i < 0 || *ret == NULL) + err(1, "xasprintf"); + + return i; +} + +int +xsnprintf(char *str, size_t len, const char *fmt, ...) +{ + va_list ap; + int i; + + va_start(ap, fmt); + i = xvsnprintf(str, len, fmt, ap); + va_end(ap); + + return i; +} + +int +xvsnprintf(char *str, size_t len, const char *fmt, va_list ap) +{ + int i; + + if (len > INT_MAX) + errx(1, "xsnprintf: len > INT_MAX"); + + i = vsnprintf(str, len, fmt, ap); + + if (i < 0 || i >= (int)len) + errx(1, "xsnprintf: overflow"); + + return i; +} diff --git a/xmalloc.h b/xmalloc.h new file mode 100644 index 0000000..76d93a2 --- /dev/null +++ b/xmalloc.h @@ -0,0 +1,41 @@ +/* $OpenBSD: xmalloc.h,v 1.2 2016/11/17 10:06:08 nicm Exp $ */ + +/* + * Author: Tatu Ylonen + * Copyright (c) 1995 Tatu Ylonen , Espoo, Finland + * All rights reserved + * Created: Mon Mar 20 22:09:17 1995 ylo + * + * Versions of malloc and friends that check their results, and never return + * failure (they call fatal if they encounter an error). + * + * As far as I am concerned, the code I have written for this software + * can be used freely for any purpose. Any derived versions of this + * software must be clearly marked as such, and if the derived work is + * incompatible with the protocol description in the RFC file, it must be + * called by a name other than "ssh" or "Secure Shell". + */ + +#ifndef XMALLOC_H +#define XMALLOC_H + +void *xmalloc(size_t); +void *xcalloc(size_t, size_t); +void *xrealloc(void *, size_t); +void *xreallocarray(void *, size_t, size_t); +char *xstrdup(const char *); +char *xstrndup(const char *, size_t); +int xasprintf(char **, const char *, ...) + __attribute__((__format__ (printf, 2, 3))) + __attribute__((__nonnull__ (2))); +int xvasprintf(char **, const char *, va_list) + __attribute__((__nonnull__ (2))); +int xsnprintf(char *, size_t, const char *, ...) + __attribute__((__format__ (printf, 3, 4))) + __attribute__((__nonnull__ (3))) + __attribute__((__bounded__ (__string__, 1, 2))); +int xvsnprintf(char *, size_t, const char *, va_list) + __attribute__((__nonnull__ (3))) + __attribute__((__bounded__ (__string__, 1, 2))); + +#endif /* XMALLOC_H */ -- cgit v1.2.3