# HG changeset patch # User Sunil Nimmagadda # Date 1670334715 0 # Node ID 1d0ce1ebbc72520482dc4ba5b21847ace09ba6b9 An HTTP(S), FTP client. Found a copy of some old OpenBSD days hacking stashed somewhere in the backups. This version saw the light of the day as official OpenBSD ftp(1) for a grand total of 1 day :-) diff -r 000000000000 -r 1d0ce1ebbc72 Makefile --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Makefile Tue Dec 06 13:51:55 2022 +0000 @@ -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 -r 000000000000 -r 1d0ce1ebbc72 cmd.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cmd.c Tue Dec 06 13:51:55 2022 +0000 @@ -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 -r 000000000000 -r 1d0ce1ebbc72 file.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/file.c Tue Dec 06 13:51:55 2022 +0000 @@ -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 -r 000000000000 -r 1d0ce1ebbc72 ftp.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ftp.1 Tue Dec 06 13:51:55 2022 +0000 @@ -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 -r 000000000000 -r 1d0ce1ebbc72 ftp.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ftp.c Tue Dec 06 13:51:55 2022 +0000 @@ -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 -r 000000000000 -r 1d0ce1ebbc72 ftp.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ftp.h Tue Dec 06 13:51:55 2022 +0000 @@ -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 -r 000000000000 -r 1d0ce1ebbc72 http.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/http.c Tue Dec 06 13:51:55 2022 +0000 @@ -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 -r 000000000000 -r 1d0ce1ebbc72 main.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/main.c Tue Dec 06 13:51:55 2022 +0000 @@ -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 -r 000000000000 -r 1d0ce1ebbc72 progressmeter.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/progressmeter.c Tue Dec 06 13:51:55 2022 +0000 @@ -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 -r 000000000000 -r 1d0ce1ebbc72 regress/Makefile --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/regress/Makefile Tue Dec 06 13:51:55 2022 +0000 @@ -0,0 +1,3 @@ +SUBDIR+= unit-tests + +.include diff -r 000000000000 -r 1d0ce1ebbc72 regress/unit-tests/Makefile --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/regress/unit-tests/Makefile Tue Dec 06 13:51:55 2022 +0000 @@ -0,0 +1,2 @@ +SUBDIR+= url_parse +. include diff -r 000000000000 -r 1d0ce1ebbc72 regress/unit-tests/url_parse/Makefile --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/regress/unit-tests/url_parse/Makefile Tue Dec 06 13:51:55 2022 +0000 @@ -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 -r 000000000000 -r 1d0ce1ebbc72 regress/unit-tests/url_parse/test_url_parse.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/regress/unit-tests/url_parse/test_url_parse.c Tue Dec 06 13:51:55 2022 +0000 @@ -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 -r 000000000000 -r 1d0ce1ebbc72 url.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/url.c Tue Dec 06 13:51:55 2022 +0000 @@ -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 -r 000000000000 -r 1d0ce1ebbc72 util.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/util.c Tue Dec 06 13:51:55 2022 +0000 @@ -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 -r 000000000000 -r 1d0ce1ebbc72 xmalloc.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xmalloc.c Tue Dec 06 13:51:55 2022 +0000 @@ -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 -r 000000000000 -r 1d0ce1ebbc72 xmalloc.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/xmalloc.h Tue Dec 06 13:51:55 2022 +0000 @@ -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 */