mirror of
git://anongit.mindrot.org/openssh.git
synced 2024-11-23 09:17:32 +08:00
81c1099d22
problematic client behaviours, controlled by two new sshd_config(5) options: PerSourcePenalties and PerSourcePenaltyExemptList. When PerSourcePenalties are enabled, sshd(8) will monitor the exit status of its child pre-auth session processes. Through the exit status, it can observe situations where the session did not authenticate as expected. These conditions include when the client repeatedly attempted authentication unsucessfully (possibly indicating an attack against one or more accounts, e.g. password guessing), or when client behaviour caused sshd to crash (possibly indicating attempts to exploit sshd). When such a condition is observed, sshd will record a penalty of some duration (e.g. 30 seconds) against the client's address. If this time is above a minimum threshold specified by the PerSourcePenalties, then connections from the client address will be refused (along with any others in the same PerSourceNetBlockSize CIDR range). Repeated offenses by the same client address will accrue greater penalties, up to a configurable maximum. A PerSourcePenaltyExemptList option allows certain address ranges to be exempt from all penalties. We hope these options will make it significantly more difficult for attackers to find accounts with weak/guessable passwords or exploit bugs in sshd(8) itself. PerSourcePenalties is off by default, but we expect to enable it automatically in the near future. much feedback markus@ and others, ok markus@ OpenBSD-Commit-ID: 89ded70eccb2b4926ef0366a4d58a693de366cca
3119 lines
66 KiB
C
3119 lines
66 KiB
C
/* $OpenBSD: misc.c,v 1.196 2024/06/06 17:15:25 djm Exp $ */
|
|
/*
|
|
* Copyright (c) 2000 Markus Friedl. All rights reserved.
|
|
* Copyright (c) 2005-2020 Damien Miller. All rights reserved.
|
|
* Copyright (c) 2004 Henning Brauer <henning@openbsd.org>
|
|
*
|
|
* 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 "includes.h"
|
|
|
|
#include <sys/types.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/time.h>
|
|
#include <sys/wait.h>
|
|
#include <sys/un.h>
|
|
|
|
#include <limits.h>
|
|
#ifdef HAVE_LIBGEN_H
|
|
# include <libgen.h>
|
|
#endif
|
|
#ifdef HAVE_POLL_H
|
|
#include <poll.h>
|
|
#endif
|
|
#ifdef HAVE_NLIST_H
|
|
#include <nlist.h>
|
|
#endif
|
|
#include <signal.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#ifdef HAVE_STDINT_H
|
|
# include <stdint.h>
|
|
#endif
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#include <netinet/in.h>
|
|
#include <netinet/in_systm.h>
|
|
#include <netinet/ip.h>
|
|
#include <netinet/tcp.h>
|
|
#include <arpa/inet.h>
|
|
|
|
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <netdb.h>
|
|
#ifdef HAVE_PATHS_H
|
|
# include <paths.h>
|
|
#include <pwd.h>
|
|
#include <grp.h>
|
|
#endif
|
|
#ifdef SSH_TUN_OPENBSD
|
|
#include <net/if.h>
|
|
#endif
|
|
|
|
#include "xmalloc.h"
|
|
#include "misc.h"
|
|
#include "log.h"
|
|
#include "ssh.h"
|
|
#include "sshbuf.h"
|
|
#include "ssherr.h"
|
|
#include "platform.h"
|
|
|
|
/* remove newline at end of string */
|
|
char *
|
|
chop(char *s)
|
|
{
|
|
char *t = s;
|
|
while (*t) {
|
|
if (*t == '\n' || *t == '\r') {
|
|
*t = '\0';
|
|
return s;
|
|
}
|
|
t++;
|
|
}
|
|
return s;
|
|
|
|
}
|
|
|
|
/* remove whitespace from end of string */
|
|
void
|
|
rtrim(char *s)
|
|
{
|
|
size_t i;
|
|
|
|
if ((i = strlen(s)) == 0)
|
|
return;
|
|
for (i--; i > 0; i--) {
|
|
if (isspace((unsigned char)s[i]))
|
|
s[i] = '\0';
|
|
}
|
|
}
|
|
|
|
/* set/unset filedescriptor to non-blocking */
|
|
int
|
|
set_nonblock(int fd)
|
|
{
|
|
int val;
|
|
|
|
val = fcntl(fd, F_GETFL);
|
|
if (val == -1) {
|
|
error("fcntl(%d, F_GETFL): %s", fd, strerror(errno));
|
|
return (-1);
|
|
}
|
|
if (val & O_NONBLOCK) {
|
|
debug3("fd %d is O_NONBLOCK", fd);
|
|
return (0);
|
|
}
|
|
debug2("fd %d setting O_NONBLOCK", fd);
|
|
val |= O_NONBLOCK;
|
|
if (fcntl(fd, F_SETFL, val) == -1) {
|
|
debug("fcntl(%d, F_SETFL, O_NONBLOCK): %s", fd,
|
|
strerror(errno));
|
|
return (-1);
|
|
}
|
|
return (0);
|
|
}
|
|
|
|
int
|
|
unset_nonblock(int fd)
|
|
{
|
|
int val;
|
|
|
|
val = fcntl(fd, F_GETFL);
|
|
if (val == -1) {
|
|
error("fcntl(%d, F_GETFL): %s", fd, strerror(errno));
|
|
return (-1);
|
|
}
|
|
if (!(val & O_NONBLOCK)) {
|
|
debug3("fd %d is not O_NONBLOCK", fd);
|
|
return (0);
|
|
}
|
|
debug("fd %d clearing O_NONBLOCK", fd);
|
|
val &= ~O_NONBLOCK;
|
|
if (fcntl(fd, F_SETFL, val) == -1) {
|
|
debug("fcntl(%d, F_SETFL, ~O_NONBLOCK): %s",
|
|
fd, strerror(errno));
|
|
return (-1);
|
|
}
|
|
return (0);
|
|
}
|
|
|
|
const char *
|
|
ssh_gai_strerror(int gaierr)
|
|
{
|
|
if (gaierr == EAI_SYSTEM && errno != 0)
|
|
return strerror(errno);
|
|
return gai_strerror(gaierr);
|
|
}
|
|
|
|
/* disable nagle on socket */
|
|
void
|
|
set_nodelay(int fd)
|
|
{
|
|
int opt;
|
|
socklen_t optlen;
|
|
|
|
optlen = sizeof opt;
|
|
if (getsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &opt, &optlen) == -1) {
|
|
debug("getsockopt TCP_NODELAY: %.100s", strerror(errno));
|
|
return;
|
|
}
|
|
if (opt == 1) {
|
|
debug2("fd %d is TCP_NODELAY", fd);
|
|
return;
|
|
}
|
|
opt = 1;
|
|
debug2("fd %d setting TCP_NODELAY", fd);
|
|
if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof opt) == -1)
|
|
error("setsockopt TCP_NODELAY: %.100s", strerror(errno));
|
|
}
|
|
|
|
/* Allow local port reuse in TIME_WAIT */
|
|
int
|
|
set_reuseaddr(int fd)
|
|
{
|
|
int on = 1;
|
|
|
|
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) {
|
|
error("setsockopt SO_REUSEADDR fd %d: %s", fd, strerror(errno));
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Get/set routing domain */
|
|
char *
|
|
get_rdomain(int fd)
|
|
{
|
|
#if defined(HAVE_SYS_GET_RDOMAIN)
|
|
return sys_get_rdomain(fd);
|
|
#elif defined(__OpenBSD__)
|
|
int rtable;
|
|
char *ret;
|
|
socklen_t len = sizeof(rtable);
|
|
|
|
if (getsockopt(fd, SOL_SOCKET, SO_RTABLE, &rtable, &len) == -1) {
|
|
error("Failed to get routing domain for fd %d: %s",
|
|
fd, strerror(errno));
|
|
return NULL;
|
|
}
|
|
xasprintf(&ret, "%d", rtable);
|
|
return ret;
|
|
#else /* defined(__OpenBSD__) */
|
|
return NULL;
|
|
#endif
|
|
}
|
|
|
|
int
|
|
set_rdomain(int fd, const char *name)
|
|
{
|
|
#if defined(HAVE_SYS_SET_RDOMAIN)
|
|
return sys_set_rdomain(fd, name);
|
|
#elif defined(__OpenBSD__)
|
|
int rtable;
|
|
const char *errstr;
|
|
|
|
if (name == NULL)
|
|
return 0; /* default table */
|
|
|
|
rtable = (int)strtonum(name, 0, 255, &errstr);
|
|
if (errstr != NULL) {
|
|
/* Shouldn't happen */
|
|
error("Invalid routing domain \"%s\": %s", name, errstr);
|
|
return -1;
|
|
}
|
|
if (setsockopt(fd, SOL_SOCKET, SO_RTABLE,
|
|
&rtable, sizeof(rtable)) == -1) {
|
|
error("Failed to set routing domain %d on fd %d: %s",
|
|
rtable, fd, strerror(errno));
|
|
return -1;
|
|
}
|
|
return 0;
|
|
#else /* defined(__OpenBSD__) */
|
|
error("Setting routing domain is not supported on this platform");
|
|
return -1;
|
|
#endif
|
|
}
|
|
|
|
int
|
|
get_sock_af(int fd)
|
|
{
|
|
struct sockaddr_storage to;
|
|
socklen_t tolen = sizeof(to);
|
|
|
|
memset(&to, 0, sizeof(to));
|
|
if (getsockname(fd, (struct sockaddr *)&to, &tolen) == -1)
|
|
return -1;
|
|
#ifdef IPV4_IN_IPV6
|
|
if (to.ss_family == AF_INET6 &&
|
|
IN6_IS_ADDR_V4MAPPED(&((struct sockaddr_in6 *)&to)->sin6_addr))
|
|
return AF_INET;
|
|
#endif
|
|
return to.ss_family;
|
|
}
|
|
|
|
void
|
|
set_sock_tos(int fd, int tos)
|
|
{
|
|
#ifndef IP_TOS_IS_BROKEN
|
|
int af;
|
|
|
|
switch ((af = get_sock_af(fd))) {
|
|
case -1:
|
|
/* assume not a socket */
|
|
break;
|
|
case AF_INET:
|
|
# ifdef IP_TOS
|
|
debug3_f("set socket %d IP_TOS 0x%02x", fd, tos);
|
|
if (setsockopt(fd, IPPROTO_IP, IP_TOS,
|
|
&tos, sizeof(tos)) == -1) {
|
|
error("setsockopt socket %d IP_TOS %d: %s",
|
|
fd, tos, strerror(errno));
|
|
}
|
|
# endif /* IP_TOS */
|
|
break;
|
|
case AF_INET6:
|
|
# ifdef IPV6_TCLASS
|
|
debug3_f("set socket %d IPV6_TCLASS 0x%02x", fd, tos);
|
|
if (setsockopt(fd, IPPROTO_IPV6, IPV6_TCLASS,
|
|
&tos, sizeof(tos)) == -1) {
|
|
error("setsockopt socket %d IPV6_TCLASS %d: %s",
|
|
fd, tos, strerror(errno));
|
|
}
|
|
# endif /* IPV6_TCLASS */
|
|
break;
|
|
default:
|
|
debug2_f("unsupported socket family %d", af);
|
|
break;
|
|
}
|
|
#endif /* IP_TOS_IS_BROKEN */
|
|
}
|
|
|
|
/*
|
|
* Wait up to *timeoutp milliseconds for events on fd. Updates
|
|
* *timeoutp with time remaining.
|
|
* Returns 0 if fd ready or -1 on timeout or error (see errno).
|
|
*/
|
|
static int
|
|
waitfd(int fd, int *timeoutp, short events, volatile sig_atomic_t *stop)
|
|
{
|
|
struct pollfd pfd;
|
|
struct timespec timeout;
|
|
int oerrno, r;
|
|
sigset_t nsigset, osigset;
|
|
|
|
if (timeoutp && *timeoutp == -1)
|
|
timeoutp = NULL;
|
|
pfd.fd = fd;
|
|
pfd.events = events;
|
|
ptimeout_init(&timeout);
|
|
if (timeoutp != NULL)
|
|
ptimeout_deadline_ms(&timeout, *timeoutp);
|
|
if (stop != NULL)
|
|
sigfillset(&nsigset);
|
|
for (; timeoutp == NULL || *timeoutp >= 0;) {
|
|
if (stop != NULL) {
|
|
sigprocmask(SIG_BLOCK, &nsigset, &osigset);
|
|
if (*stop) {
|
|
sigprocmask(SIG_SETMASK, &osigset, NULL);
|
|
errno = EINTR;
|
|
return -1;
|
|
}
|
|
}
|
|
r = ppoll(&pfd, 1, ptimeout_get_tsp(&timeout),
|
|
stop != NULL ? &osigset : NULL);
|
|
oerrno = errno;
|
|
if (stop != NULL)
|
|
sigprocmask(SIG_SETMASK, &osigset, NULL);
|
|
if (timeoutp)
|
|
*timeoutp = ptimeout_get_ms(&timeout);
|
|
errno = oerrno;
|
|
if (r > 0)
|
|
return 0;
|
|
else if (r == -1 && errno != EAGAIN && errno != EINTR)
|
|
return -1;
|
|
else if (r == 0)
|
|
break;
|
|
}
|
|
/* timeout */
|
|
errno = ETIMEDOUT;
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* Wait up to *timeoutp milliseconds for fd to be readable. Updates
|
|
* *timeoutp with time remaining.
|
|
* Returns 0 if fd ready or -1 on timeout or error (see errno).
|
|
*/
|
|
int
|
|
waitrfd(int fd, int *timeoutp, volatile sig_atomic_t *stop) {
|
|
return waitfd(fd, timeoutp, POLLIN, stop);
|
|
}
|
|
|
|
/*
|
|
* Attempt a non-blocking connect(2) to the specified address, waiting up to
|
|
* *timeoutp milliseconds for the connection to complete. If the timeout is
|
|
* <=0, then wait indefinitely.
|
|
*
|
|
* Returns 0 on success or -1 on failure.
|
|
*/
|
|
int
|
|
timeout_connect(int sockfd, const struct sockaddr *serv_addr,
|
|
socklen_t addrlen, int *timeoutp)
|
|
{
|
|
int optval = 0;
|
|
socklen_t optlen = sizeof(optval);
|
|
|
|
/* No timeout: just do a blocking connect() */
|
|
if (timeoutp == NULL || *timeoutp <= 0)
|
|
return connect(sockfd, serv_addr, addrlen);
|
|
|
|
set_nonblock(sockfd);
|
|
for (;;) {
|
|
if (connect(sockfd, serv_addr, addrlen) == 0) {
|
|
/* Succeeded already? */
|
|
unset_nonblock(sockfd);
|
|
return 0;
|
|
} else if (errno == EINTR)
|
|
continue;
|
|
else if (errno != EINPROGRESS)
|
|
return -1;
|
|
break;
|
|
}
|
|
|
|
if (waitfd(sockfd, timeoutp, POLLIN | POLLOUT, NULL) == -1)
|
|
return -1;
|
|
|
|
/* Completed or failed */
|
|
if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &optval, &optlen) == -1) {
|
|
debug("getsockopt: %s", strerror(errno));
|
|
return -1;
|
|
}
|
|
if (optval != 0) {
|
|
errno = optval;
|
|
return -1;
|
|
}
|
|
unset_nonblock(sockfd);
|
|
return 0;
|
|
}
|
|
|
|
/* Characters considered whitespace in strsep calls. */
|
|
#define WHITESPACE " \t\r\n"
|
|
#define QUOTE "\""
|
|
|
|
/* return next token in configuration line */
|
|
static char *
|
|
strdelim_internal(char **s, int split_equals)
|
|
{
|
|
char *old;
|
|
int wspace = 0;
|
|
|
|
if (*s == NULL)
|
|
return NULL;
|
|
|
|
old = *s;
|
|
|
|
*s = strpbrk(*s,
|
|
split_equals ? WHITESPACE QUOTE "=" : WHITESPACE QUOTE);
|
|
if (*s == NULL)
|
|
return (old);
|
|
|
|
if (*s[0] == '\"') {
|
|
memmove(*s, *s + 1, strlen(*s)); /* move nul too */
|
|
/* Find matching quote */
|
|
if ((*s = strpbrk(*s, QUOTE)) == NULL) {
|
|
return (NULL); /* no matching quote */
|
|
} else {
|
|
*s[0] = '\0';
|
|
*s += strspn(*s + 1, WHITESPACE) + 1;
|
|
return (old);
|
|
}
|
|
}
|
|
|
|
/* Allow only one '=' to be skipped */
|
|
if (split_equals && *s[0] == '=')
|
|
wspace = 1;
|
|
*s[0] = '\0';
|
|
|
|
/* Skip any extra whitespace after first token */
|
|
*s += strspn(*s + 1, WHITESPACE) + 1;
|
|
if (split_equals && *s[0] == '=' && !wspace)
|
|
*s += strspn(*s + 1, WHITESPACE) + 1;
|
|
|
|
return (old);
|
|
}
|
|
|
|
/*
|
|
* Return next token in configuration line; splts on whitespace or a
|
|
* single '=' character.
|
|
*/
|
|
char *
|
|
strdelim(char **s)
|
|
{
|
|
return strdelim_internal(s, 1);
|
|
}
|
|
|
|
/*
|
|
* Return next token in configuration line; splts on whitespace only.
|
|
*/
|
|
char *
|
|
strdelimw(char **s)
|
|
{
|
|
return strdelim_internal(s, 0);
|
|
}
|
|
|
|
struct passwd *
|
|
pwcopy(struct passwd *pw)
|
|
{
|
|
struct passwd *copy = xcalloc(1, sizeof(*copy));
|
|
|
|
copy->pw_name = xstrdup(pw->pw_name);
|
|
copy->pw_passwd = xstrdup(pw->pw_passwd == NULL ? "*" : pw->pw_passwd);
|
|
#ifdef HAVE_STRUCT_PASSWD_PW_GECOS
|
|
copy->pw_gecos = xstrdup(pw->pw_gecos);
|
|
#endif
|
|
copy->pw_uid = pw->pw_uid;
|
|
copy->pw_gid = pw->pw_gid;
|
|
#ifdef HAVE_STRUCT_PASSWD_PW_EXPIRE
|
|
copy->pw_expire = pw->pw_expire;
|
|
#endif
|
|
#ifdef HAVE_STRUCT_PASSWD_PW_CHANGE
|
|
copy->pw_change = pw->pw_change;
|
|
#endif
|
|
#ifdef HAVE_STRUCT_PASSWD_PW_CLASS
|
|
copy->pw_class = xstrdup(pw->pw_class);
|
|
#endif
|
|
copy->pw_dir = xstrdup(pw->pw_dir);
|
|
copy->pw_shell = xstrdup(pw->pw_shell);
|
|
return copy;
|
|
}
|
|
|
|
/*
|
|
* Convert ASCII string to TCP/IP port number.
|
|
* Port must be >=0 and <=65535.
|
|
* Return -1 if invalid.
|
|
*/
|
|
int
|
|
a2port(const char *s)
|
|
{
|
|
struct servent *se;
|
|
long long port;
|
|
const char *errstr;
|
|
|
|
port = strtonum(s, 0, 65535, &errstr);
|
|
if (errstr == NULL)
|
|
return (int)port;
|
|
if ((se = getservbyname(s, "tcp")) != NULL)
|
|
return ntohs(se->s_port);
|
|
return -1;
|
|
}
|
|
|
|
int
|
|
a2tun(const char *s, int *remote)
|
|
{
|
|
const char *errstr = NULL;
|
|
char *sp, *ep;
|
|
int tun;
|
|
|
|
if (remote != NULL) {
|
|
*remote = SSH_TUNID_ANY;
|
|
sp = xstrdup(s);
|
|
if ((ep = strchr(sp, ':')) == NULL) {
|
|
free(sp);
|
|
return (a2tun(s, NULL));
|
|
}
|
|
ep[0] = '\0'; ep++;
|
|
*remote = a2tun(ep, NULL);
|
|
tun = a2tun(sp, NULL);
|
|
free(sp);
|
|
return (*remote == SSH_TUNID_ERR ? *remote : tun);
|
|
}
|
|
|
|
if (strcasecmp(s, "any") == 0)
|
|
return (SSH_TUNID_ANY);
|
|
|
|
tun = strtonum(s, 0, SSH_TUNID_MAX, &errstr);
|
|
if (errstr != NULL)
|
|
return (SSH_TUNID_ERR);
|
|
|
|
return (tun);
|
|
}
|
|
|
|
#define SECONDS 1
|
|
#define MINUTES (SECONDS * 60)
|
|
#define HOURS (MINUTES * 60)
|
|
#define DAYS (HOURS * 24)
|
|
#define WEEKS (DAYS * 7)
|
|
|
|
static char *
|
|
scandigits(char *s)
|
|
{
|
|
while (isdigit((unsigned char)*s))
|
|
s++;
|
|
return s;
|
|
}
|
|
|
|
/*
|
|
* Convert a time string into seconds; format is
|
|
* a sequence of:
|
|
* time[qualifier]
|
|
*
|
|
* Valid time qualifiers are:
|
|
* <none> seconds
|
|
* s|S seconds
|
|
* m|M minutes
|
|
* h|H hours
|
|
* d|D days
|
|
* w|W weeks
|
|
*
|
|
* Examples:
|
|
* 90m 90 minutes
|
|
* 1h30m 90 minutes
|
|
* 2d 2 days
|
|
* 1w 1 week
|
|
*
|
|
* Return -1 if time string is invalid.
|
|
*/
|
|
int
|
|
convtime(const char *s)
|
|
{
|
|
int secs, total = 0, multiplier;
|
|
char *p, *os, *np, c = 0;
|
|
const char *errstr;
|
|
|
|
if (s == NULL || *s == '\0')
|
|
return -1;
|
|
p = os = strdup(s); /* deal with const */
|
|
if (os == NULL)
|
|
return -1;
|
|
|
|
while (*p) {
|
|
np = scandigits(p);
|
|
if (np) {
|
|
c = *np;
|
|
*np = '\0';
|
|
}
|
|
secs = (int)strtonum(p, 0, INT_MAX, &errstr);
|
|
if (errstr)
|
|
goto fail;
|
|
*np = c;
|
|
|
|
multiplier = 1;
|
|
switch (c) {
|
|
case '\0':
|
|
np--; /* back up */
|
|
break;
|
|
case 's':
|
|
case 'S':
|
|
break;
|
|
case 'm':
|
|
case 'M':
|
|
multiplier = MINUTES;
|
|
break;
|
|
case 'h':
|
|
case 'H':
|
|
multiplier = HOURS;
|
|
break;
|
|
case 'd':
|
|
case 'D':
|
|
multiplier = DAYS;
|
|
break;
|
|
case 'w':
|
|
case 'W':
|
|
multiplier = WEEKS;
|
|
break;
|
|
default:
|
|
goto fail;
|
|
}
|
|
if (secs > INT_MAX / multiplier)
|
|
goto fail;
|
|
secs *= multiplier;
|
|
if (total > INT_MAX - secs)
|
|
goto fail;
|
|
total += secs;
|
|
if (total < 0)
|
|
goto fail;
|
|
p = ++np;
|
|
}
|
|
free(os);
|
|
return total;
|
|
fail:
|
|
free(os);
|
|
return -1;
|
|
}
|
|
|
|
#define TF_BUFS 8
|
|
#define TF_LEN 9
|
|
|
|
const char *
|
|
fmt_timeframe(time_t t)
|
|
{
|
|
char *buf;
|
|
static char tfbuf[TF_BUFS][TF_LEN]; /* ring buffer */
|
|
static int idx = 0;
|
|
unsigned int sec, min, hrs, day;
|
|
unsigned long long week;
|
|
|
|
buf = tfbuf[idx++];
|
|
if (idx == TF_BUFS)
|
|
idx = 0;
|
|
|
|
week = t;
|
|
|
|
sec = week % 60;
|
|
week /= 60;
|
|
min = week % 60;
|
|
week /= 60;
|
|
hrs = week % 24;
|
|
week /= 24;
|
|
day = week % 7;
|
|
week /= 7;
|
|
|
|
if (week > 0)
|
|
snprintf(buf, TF_LEN, "%02lluw%01ud%02uh", week, day, hrs);
|
|
else if (day > 0)
|
|
snprintf(buf, TF_LEN, "%01ud%02uh%02um", day, hrs, min);
|
|
else
|
|
snprintf(buf, TF_LEN, "%02u:%02u:%02u", hrs, min, sec);
|
|
|
|
return (buf);
|
|
}
|
|
|
|
/*
|
|
* Returns a standardized host+port identifier string.
|
|
* Caller must free returned string.
|
|
*/
|
|
char *
|
|
put_host_port(const char *host, u_short port)
|
|
{
|
|
char *hoststr;
|
|
|
|
if (port == 0 || port == SSH_DEFAULT_PORT)
|
|
return(xstrdup(host));
|
|
if (asprintf(&hoststr, "[%s]:%d", host, (int)port) == -1)
|
|
fatal("put_host_port: asprintf: %s", strerror(errno));
|
|
debug3("put_host_port: %s", hoststr);
|
|
return hoststr;
|
|
}
|
|
|
|
/*
|
|
* Search for next delimiter between hostnames/addresses and ports.
|
|
* Argument may be modified (for termination).
|
|
* Returns *cp if parsing succeeds.
|
|
* *cp is set to the start of the next field, if one was found.
|
|
* The delimiter char, if present, is stored in delim.
|
|
* If this is the last field, *cp is set to NULL.
|
|
*/
|
|
char *
|
|
hpdelim2(char **cp, char *delim)
|
|
{
|
|
char *s, *old;
|
|
|
|
if (cp == NULL || *cp == NULL)
|
|
return NULL;
|
|
|
|
old = s = *cp;
|
|
if (*s == '[') {
|
|
if ((s = strchr(s, ']')) == NULL)
|
|
return NULL;
|
|
else
|
|
s++;
|
|
} else if ((s = strpbrk(s, ":/")) == NULL)
|
|
s = *cp + strlen(*cp); /* skip to end (see first case below) */
|
|
|
|
switch (*s) {
|
|
case '\0':
|
|
*cp = NULL; /* no more fields*/
|
|
break;
|
|
|
|
case ':':
|
|
case '/':
|
|
if (delim != NULL)
|
|
*delim = *s;
|
|
*s = '\0'; /* terminate */
|
|
*cp = s + 1;
|
|
break;
|
|
|
|
default:
|
|
return NULL;
|
|
}
|
|
|
|
return old;
|
|
}
|
|
|
|
/* The common case: only accept colon as delimiter. */
|
|
char *
|
|
hpdelim(char **cp)
|
|
{
|
|
char *r, delim = '\0';
|
|
|
|
r = hpdelim2(cp, &delim);
|
|
if (delim == '/')
|
|
return NULL;
|
|
return r;
|
|
}
|
|
|
|
char *
|
|
cleanhostname(char *host)
|
|
{
|
|
if (*host == '[' && host[strlen(host) - 1] == ']') {
|
|
host[strlen(host) - 1] = '\0';
|
|
return (host + 1);
|
|
} else
|
|
return host;
|
|
}
|
|
|
|
char *
|
|
colon(char *cp)
|
|
{
|
|
int flag = 0;
|
|
|
|
if (*cp == ':') /* Leading colon is part of file name. */
|
|
return NULL;
|
|
if (*cp == '[')
|
|
flag = 1;
|
|
|
|
for (; *cp; ++cp) {
|
|
if (*cp == '@' && *(cp+1) == '[')
|
|
flag = 1;
|
|
if (*cp == ']' && *(cp+1) == ':' && flag)
|
|
return (cp+1);
|
|
if (*cp == ':' && !flag)
|
|
return (cp);
|
|
if (*cp == '/')
|
|
return NULL;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
* Parse a [user@]host:[path] string.
|
|
* Caller must free returned user, host and path.
|
|
* Any of the pointer return arguments may be NULL (useful for syntax checking).
|
|
* If user was not specified then *userp will be set to NULL.
|
|
* If host was not specified then *hostp will be set to NULL.
|
|
* If path was not specified then *pathp will be set to ".".
|
|
* Returns 0 on success, -1 on failure.
|
|
*/
|
|
int
|
|
parse_user_host_path(const char *s, char **userp, char **hostp, char **pathp)
|
|
{
|
|
char *user = NULL, *host = NULL, *path = NULL;
|
|
char *sdup, *tmp;
|
|
int ret = -1;
|
|
|
|
if (userp != NULL)
|
|
*userp = NULL;
|
|
if (hostp != NULL)
|
|
*hostp = NULL;
|
|
if (pathp != NULL)
|
|
*pathp = NULL;
|
|
|
|
sdup = xstrdup(s);
|
|
|
|
/* Check for remote syntax: [user@]host:[path] */
|
|
if ((tmp = colon(sdup)) == NULL)
|
|
goto out;
|
|
|
|
/* Extract optional path */
|
|
*tmp++ = '\0';
|
|
if (*tmp == '\0')
|
|
tmp = ".";
|
|
path = xstrdup(tmp);
|
|
|
|
/* Extract optional user and mandatory host */
|
|
tmp = strrchr(sdup, '@');
|
|
if (tmp != NULL) {
|
|
*tmp++ = '\0';
|
|
host = xstrdup(cleanhostname(tmp));
|
|
if (*sdup != '\0')
|
|
user = xstrdup(sdup);
|
|
} else {
|
|
host = xstrdup(cleanhostname(sdup));
|
|
user = NULL;
|
|
}
|
|
|
|
/* Success */
|
|
if (userp != NULL) {
|
|
*userp = user;
|
|
user = NULL;
|
|
}
|
|
if (hostp != NULL) {
|
|
*hostp = host;
|
|
host = NULL;
|
|
}
|
|
if (pathp != NULL) {
|
|
*pathp = path;
|
|
path = NULL;
|
|
}
|
|
ret = 0;
|
|
out:
|
|
free(sdup);
|
|
free(user);
|
|
free(host);
|
|
free(path);
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Parse a [user@]host[:port] string.
|
|
* Caller must free returned user and host.
|
|
* Any of the pointer return arguments may be NULL (useful for syntax checking).
|
|
* If user was not specified then *userp will be set to NULL.
|
|
* If port was not specified then *portp will be -1.
|
|
* Returns 0 on success, -1 on failure.
|
|
*/
|
|
int
|
|
parse_user_host_port(const char *s, char **userp, char **hostp, int *portp)
|
|
{
|
|
char *sdup, *cp, *tmp;
|
|
char *user = NULL, *host = NULL;
|
|
int port = -1, ret = -1;
|
|
|
|
if (userp != NULL)
|
|
*userp = NULL;
|
|
if (hostp != NULL)
|
|
*hostp = NULL;
|
|
if (portp != NULL)
|
|
*portp = -1;
|
|
|
|
if ((sdup = tmp = strdup(s)) == NULL)
|
|
return -1;
|
|
/* Extract optional username */
|
|
if ((cp = strrchr(tmp, '@')) != NULL) {
|
|
*cp = '\0';
|
|
if (*tmp == '\0')
|
|
goto out;
|
|
if ((user = strdup(tmp)) == NULL)
|
|
goto out;
|
|
tmp = cp + 1;
|
|
}
|
|
/* Extract mandatory hostname */
|
|
if ((cp = hpdelim(&tmp)) == NULL || *cp == '\0')
|
|
goto out;
|
|
host = xstrdup(cleanhostname(cp));
|
|
/* Convert and verify optional port */
|
|
if (tmp != NULL && *tmp != '\0') {
|
|
if ((port = a2port(tmp)) <= 0)
|
|
goto out;
|
|
}
|
|
/* Success */
|
|
if (userp != NULL) {
|
|
*userp = user;
|
|
user = NULL;
|
|
}
|
|
if (hostp != NULL) {
|
|
*hostp = host;
|
|
host = NULL;
|
|
}
|
|
if (portp != NULL)
|
|
*portp = port;
|
|
ret = 0;
|
|
out:
|
|
free(sdup);
|
|
free(user);
|
|
free(host);
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Converts a two-byte hex string to decimal.
|
|
* Returns the decimal value or -1 for invalid input.
|
|
*/
|
|
static int
|
|
hexchar(const char *s)
|
|
{
|
|
unsigned char result[2];
|
|
int i;
|
|
|
|
for (i = 0; i < 2; i++) {
|
|
if (s[i] >= '0' && s[i] <= '9')
|
|
result[i] = (unsigned char)(s[i] - '0');
|
|
else if (s[i] >= 'a' && s[i] <= 'f')
|
|
result[i] = (unsigned char)(s[i] - 'a') + 10;
|
|
else if (s[i] >= 'A' && s[i] <= 'F')
|
|
result[i] = (unsigned char)(s[i] - 'A') + 10;
|
|
else
|
|
return -1;
|
|
}
|
|
return (result[0] << 4) | result[1];
|
|
}
|
|
|
|
/*
|
|
* Decode an url-encoded string.
|
|
* Returns a newly allocated string on success or NULL on failure.
|
|
*/
|
|
static char *
|
|
urldecode(const char *src)
|
|
{
|
|
char *ret, *dst;
|
|
int ch;
|
|
size_t srclen;
|
|
|
|
if ((srclen = strlen(src)) >= SIZE_MAX)
|
|
fatal_f("input too large");
|
|
ret = xmalloc(srclen + 1);
|
|
for (dst = ret; *src != '\0'; src++) {
|
|
switch (*src) {
|
|
case '+':
|
|
*dst++ = ' ';
|
|
break;
|
|
case '%':
|
|
if (!isxdigit((unsigned char)src[1]) ||
|
|
!isxdigit((unsigned char)src[2]) ||
|
|
(ch = hexchar(src + 1)) == -1) {
|
|
free(ret);
|
|
return NULL;
|
|
}
|
|
*dst++ = ch;
|
|
src += 2;
|
|
break;
|
|
default:
|
|
*dst++ = *src;
|
|
break;
|
|
}
|
|
}
|
|
*dst = '\0';
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Parse an (scp|ssh|sftp)://[user@]host[:port][/path] URI.
|
|
* See https://tools.ietf.org/html/draft-ietf-secsh-scp-sftp-ssh-uri-04
|
|
* Either user or path may be url-encoded (but not host or port).
|
|
* Caller must free returned user, host and path.
|
|
* Any of the pointer return arguments may be NULL (useful for syntax checking)
|
|
* but the scheme must always be specified.
|
|
* If user was not specified then *userp will be set to NULL.
|
|
* If port was not specified then *portp will be -1.
|
|
* If path was not specified then *pathp will be set to NULL.
|
|
* Returns 0 on success, 1 if non-uri/wrong scheme, -1 on error/invalid uri.
|
|
*/
|
|
int
|
|
parse_uri(const char *scheme, const char *uri, char **userp, char **hostp,
|
|
int *portp, char **pathp)
|
|
{
|
|
char *uridup, *cp, *tmp, ch;
|
|
char *user = NULL, *host = NULL, *path = NULL;
|
|
int port = -1, ret = -1;
|
|
size_t len;
|
|
|
|
len = strlen(scheme);
|
|
if (strncmp(uri, scheme, len) != 0 || strncmp(uri + len, "://", 3) != 0)
|
|
return 1;
|
|
uri += len + 3;
|
|
|
|
if (userp != NULL)
|
|
*userp = NULL;
|
|
if (hostp != NULL)
|
|
*hostp = NULL;
|
|
if (portp != NULL)
|
|
*portp = -1;
|
|
if (pathp != NULL)
|
|
*pathp = NULL;
|
|
|
|
uridup = tmp = xstrdup(uri);
|
|
|
|
/* Extract optional ssh-info (username + connection params) */
|
|
if ((cp = strchr(tmp, '@')) != NULL) {
|
|
char *delim;
|
|
|
|
*cp = '\0';
|
|
/* Extract username and connection params */
|
|
if ((delim = strchr(tmp, ';')) != NULL) {
|
|
/* Just ignore connection params for now */
|
|
*delim = '\0';
|
|
}
|
|
if (*tmp == '\0') {
|
|
/* Empty username */
|
|
goto out;
|
|
}
|
|
if ((user = urldecode(tmp)) == NULL)
|
|
goto out;
|
|
tmp = cp + 1;
|
|
}
|
|
|
|
/* Extract mandatory hostname */
|
|
if ((cp = hpdelim2(&tmp, &ch)) == NULL || *cp == '\0')
|
|
goto out;
|
|
host = xstrdup(cleanhostname(cp));
|
|
if (!valid_domain(host, 0, NULL))
|
|
goto out;
|
|
|
|
if (tmp != NULL && *tmp != '\0') {
|
|
if (ch == ':') {
|
|
/* Convert and verify port. */
|
|
if ((cp = strchr(tmp, '/')) != NULL)
|
|
*cp = '\0';
|
|
if ((port = a2port(tmp)) <= 0)
|
|
goto out;
|
|
tmp = cp ? cp + 1 : NULL;
|
|
}
|
|
if (tmp != NULL && *tmp != '\0') {
|
|
/* Extract optional path */
|
|
if ((path = urldecode(tmp)) == NULL)
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
/* Success */
|
|
if (userp != NULL) {
|
|
*userp = user;
|
|
user = NULL;
|
|
}
|
|
if (hostp != NULL) {
|
|
*hostp = host;
|
|
host = NULL;
|
|
}
|
|
if (portp != NULL)
|
|
*portp = port;
|
|
if (pathp != NULL) {
|
|
*pathp = path;
|
|
path = NULL;
|
|
}
|
|
ret = 0;
|
|
out:
|
|
free(uridup);
|
|
free(user);
|
|
free(host);
|
|
free(path);
|
|
return ret;
|
|
}
|
|
|
|
/* function to assist building execv() arguments */
|
|
void
|
|
addargs(arglist *args, char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
char *cp;
|
|
u_int nalloc;
|
|
int r;
|
|
|
|
va_start(ap, fmt);
|
|
r = vasprintf(&cp, fmt, ap);
|
|
va_end(ap);
|
|
if (r == -1)
|
|
fatal_f("argument too long");
|
|
|
|
nalloc = args->nalloc;
|
|
if (args->list == NULL) {
|
|
nalloc = 32;
|
|
args->num = 0;
|
|
} else if (args->num > (256 * 1024))
|
|
fatal_f("too many arguments");
|
|
else if (args->num >= args->nalloc)
|
|
fatal_f("arglist corrupt");
|
|
else if (args->num+2 >= nalloc)
|
|
nalloc *= 2;
|
|
|
|
args->list = xrecallocarray(args->list, args->nalloc,
|
|
nalloc, sizeof(char *));
|
|
args->nalloc = nalloc;
|
|
args->list[args->num++] = cp;
|
|
args->list[args->num] = NULL;
|
|
}
|
|
|
|
void
|
|
replacearg(arglist *args, u_int which, char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
char *cp;
|
|
int r;
|
|
|
|
va_start(ap, fmt);
|
|
r = vasprintf(&cp, fmt, ap);
|
|
va_end(ap);
|
|
if (r == -1)
|
|
fatal_f("argument too long");
|
|
if (args->list == NULL || args->num >= args->nalloc)
|
|
fatal_f("arglist corrupt");
|
|
|
|
if (which >= args->num)
|
|
fatal_f("tried to replace invalid arg %d >= %d",
|
|
which, args->num);
|
|
free(args->list[which]);
|
|
args->list[which] = cp;
|
|
}
|
|
|
|
void
|
|
freeargs(arglist *args)
|
|
{
|
|
u_int i;
|
|
|
|
if (args == NULL)
|
|
return;
|
|
if (args->list != NULL && args->num < args->nalloc) {
|
|
for (i = 0; i < args->num; i++)
|
|
free(args->list[i]);
|
|
free(args->list);
|
|
}
|
|
args->nalloc = args->num = 0;
|
|
args->list = NULL;
|
|
}
|
|
|
|
/*
|
|
* Expands tildes in the file name. Returns data allocated by xmalloc.
|
|
* Warning: this calls getpw*.
|
|
*/
|
|
int
|
|
tilde_expand(const char *filename, uid_t uid, char **retp)
|
|
{
|
|
char *ocopy = NULL, *copy, *s = NULL;
|
|
const char *path = NULL, *user = NULL;
|
|
struct passwd *pw;
|
|
size_t len;
|
|
int ret = -1, r, slash;
|
|
|
|
*retp = NULL;
|
|
if (*filename != '~') {
|
|
*retp = xstrdup(filename);
|
|
return 0;
|
|
}
|
|
ocopy = copy = xstrdup(filename + 1);
|
|
|
|
if (*copy == '\0') /* ~ */
|
|
path = NULL;
|
|
else if (*copy == '/') {
|
|
copy += strspn(copy, "/");
|
|
if (*copy == '\0')
|
|
path = NULL; /* ~/ */
|
|
else
|
|
path = copy; /* ~/path */
|
|
} else {
|
|
user = copy;
|
|
if ((path = strchr(copy, '/')) != NULL) {
|
|
copy[path - copy] = '\0';
|
|
path++;
|
|
path += strspn(path, "/");
|
|
if (*path == '\0') /* ~user/ */
|
|
path = NULL;
|
|
/* else ~user/path */
|
|
}
|
|
/* else ~user */
|
|
}
|
|
if (user != NULL) {
|
|
if ((pw = getpwnam(user)) == NULL) {
|
|
error_f("No such user %s", user);
|
|
goto out;
|
|
}
|
|
} else if ((pw = getpwuid(uid)) == NULL) {
|
|
error_f("No such uid %ld", (long)uid);
|
|
goto out;
|
|
}
|
|
|
|
/* Make sure directory has a trailing '/' */
|
|
slash = (len = strlen(pw->pw_dir)) == 0 || pw->pw_dir[len - 1] != '/';
|
|
|
|
if ((r = xasprintf(&s, "%s%s%s", pw->pw_dir,
|
|
slash ? "/" : "", path != NULL ? path : "")) <= 0) {
|
|
error_f("xasprintf failed");
|
|
goto out;
|
|
}
|
|
if (r >= PATH_MAX) {
|
|
error_f("Path too long");
|
|
goto out;
|
|
}
|
|
/* success */
|
|
ret = 0;
|
|
*retp = s;
|
|
s = NULL;
|
|
out:
|
|
free(s);
|
|
free(ocopy);
|
|
return ret;
|
|
}
|
|
|
|
char *
|
|
tilde_expand_filename(const char *filename, uid_t uid)
|
|
{
|
|
char *ret;
|
|
|
|
if (tilde_expand(filename, uid, &ret) != 0)
|
|
cleanup_exit(255);
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Expand a string with a set of %[char] escapes and/or ${ENVIRONMENT}
|
|
* substitutions. A number of escapes may be specified as
|
|
* (char *escape_chars, char *replacement) pairs. The list must be terminated
|
|
* by a NULL escape_char. Returns replaced string in memory allocated by
|
|
* xmalloc which the caller must free.
|
|
*/
|
|
static char *
|
|
vdollar_percent_expand(int *parseerror, int dollar, int percent,
|
|
const char *string, va_list ap)
|
|
{
|
|
#define EXPAND_MAX_KEYS 64
|
|
u_int num_keys = 0, i;
|
|
struct {
|
|
const char *key;
|
|
const char *repl;
|
|
} keys[EXPAND_MAX_KEYS];
|
|
struct sshbuf *buf;
|
|
int r, missingvar = 0;
|
|
char *ret = NULL, *var, *varend, *val;
|
|
size_t len;
|
|
|
|
if ((buf = sshbuf_new()) == NULL)
|
|
fatal_f("sshbuf_new failed");
|
|
if (parseerror == NULL)
|
|
fatal_f("null parseerror arg");
|
|
*parseerror = 1;
|
|
|
|
/* Gather keys if we're doing percent expansion. */
|
|
if (percent) {
|
|
for (num_keys = 0; num_keys < EXPAND_MAX_KEYS; num_keys++) {
|
|
keys[num_keys].key = va_arg(ap, char *);
|
|
if (keys[num_keys].key == NULL)
|
|
break;
|
|
keys[num_keys].repl = va_arg(ap, char *);
|
|
if (keys[num_keys].repl == NULL) {
|
|
fatal_f("NULL replacement for token %s",
|
|
keys[num_keys].key);
|
|
}
|
|
}
|
|
if (num_keys == EXPAND_MAX_KEYS && va_arg(ap, char *) != NULL)
|
|
fatal_f("too many keys");
|
|
if (num_keys == 0)
|
|
fatal_f("percent expansion without token list");
|
|
}
|
|
|
|
/* Expand string */
|
|
for (i = 0; *string != '\0'; string++) {
|
|
/* Optionally process ${ENVIRONMENT} expansions. */
|
|
if (dollar && string[0] == '$' && string[1] == '{') {
|
|
string += 2; /* skip over '${' */
|
|
if ((varend = strchr(string, '}')) == NULL) {
|
|
error_f("environment variable '%s' missing "
|
|
"closing '}'", string);
|
|
goto out;
|
|
}
|
|
len = varend - string;
|
|
if (len == 0) {
|
|
error_f("zero-length environment variable");
|
|
goto out;
|
|
}
|
|
var = xmalloc(len + 1);
|
|
(void)strlcpy(var, string, len + 1);
|
|
if ((val = getenv(var)) == NULL) {
|
|
error_f("env var ${%s} has no value", var);
|
|
missingvar = 1;
|
|
} else {
|
|
debug3_f("expand ${%s} -> '%s'", var, val);
|
|
if ((r = sshbuf_put(buf, val, strlen(val))) !=0)
|
|
fatal_fr(r, "sshbuf_put ${}");
|
|
}
|
|
free(var);
|
|
string += len;
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* Process percent expansions if we have a list of TOKENs.
|
|
* If we're not doing percent expansion everything just gets
|
|
* appended here.
|
|
*/
|
|
if (*string != '%' || !percent) {
|
|
append:
|
|
if ((r = sshbuf_put_u8(buf, *string)) != 0)
|
|
fatal_fr(r, "sshbuf_put_u8 %%");
|
|
continue;
|
|
}
|
|
string++;
|
|
/* %% case */
|
|
if (*string == '%')
|
|
goto append;
|
|
if (*string == '\0') {
|
|
error_f("invalid format");
|
|
goto out;
|
|
}
|
|
for (i = 0; i < num_keys; i++) {
|
|
if (strchr(keys[i].key, *string) != NULL) {
|
|
if ((r = sshbuf_put(buf, keys[i].repl,
|
|
strlen(keys[i].repl))) != 0)
|
|
fatal_fr(r, "sshbuf_put %%-repl");
|
|
break;
|
|
}
|
|
}
|
|
if (i >= num_keys) {
|
|
error_f("unknown key %%%c", *string);
|
|
goto out;
|
|
}
|
|
}
|
|
if (!missingvar && (ret = sshbuf_dup_string(buf)) == NULL)
|
|
fatal_f("sshbuf_dup_string failed");
|
|
*parseerror = 0;
|
|
out:
|
|
sshbuf_free(buf);
|
|
return *parseerror ? NULL : ret;
|
|
#undef EXPAND_MAX_KEYS
|
|
}
|
|
|
|
/*
|
|
* Expand only environment variables.
|
|
* Note that although this function is variadic like the other similar
|
|
* functions, any such arguments will be unused.
|
|
*/
|
|
|
|
char *
|
|
dollar_expand(int *parseerr, const char *string, ...)
|
|
{
|
|
char *ret;
|
|
int err;
|
|
va_list ap;
|
|
|
|
va_start(ap, string);
|
|
ret = vdollar_percent_expand(&err, 1, 0, string, ap);
|
|
va_end(ap);
|
|
if (parseerr != NULL)
|
|
*parseerr = err;
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Returns expanded string or NULL if a specified environment variable is
|
|
* not defined, or calls fatal if the string is invalid.
|
|
*/
|
|
char *
|
|
percent_expand(const char *string, ...)
|
|
{
|
|
char *ret;
|
|
int err;
|
|
va_list ap;
|
|
|
|
va_start(ap, string);
|
|
ret = vdollar_percent_expand(&err, 0, 1, string, ap);
|
|
va_end(ap);
|
|
if (err)
|
|
fatal_f("failed");
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Returns expanded string or NULL if a specified environment variable is
|
|
* not defined, or calls fatal if the string is invalid.
|
|
*/
|
|
char *
|
|
percent_dollar_expand(const char *string, ...)
|
|
{
|
|
char *ret;
|
|
int err;
|
|
va_list ap;
|
|
|
|
va_start(ap, string);
|
|
ret = vdollar_percent_expand(&err, 1, 1, string, ap);
|
|
va_end(ap);
|
|
if (err)
|
|
fatal_f("failed");
|
|
return ret;
|
|
}
|
|
|
|
int
|
|
tun_open(int tun, int mode, char **ifname)
|
|
{
|
|
#if defined(CUSTOM_SYS_TUN_OPEN)
|
|
return (sys_tun_open(tun, mode, ifname));
|
|
#elif defined(SSH_TUN_OPENBSD)
|
|
struct ifreq ifr;
|
|
char name[100];
|
|
int fd = -1, sock;
|
|
const char *tunbase = "tun";
|
|
|
|
if (ifname != NULL)
|
|
*ifname = NULL;
|
|
|
|
if (mode == SSH_TUNMODE_ETHERNET)
|
|
tunbase = "tap";
|
|
|
|
/* Open the tunnel device */
|
|
if (tun <= SSH_TUNID_MAX) {
|
|
snprintf(name, sizeof(name), "/dev/%s%d", tunbase, tun);
|
|
fd = open(name, O_RDWR);
|
|
} else if (tun == SSH_TUNID_ANY) {
|
|
for (tun = 100; tun >= 0; tun--) {
|
|
snprintf(name, sizeof(name), "/dev/%s%d",
|
|
tunbase, tun);
|
|
if ((fd = open(name, O_RDWR)) >= 0)
|
|
break;
|
|
}
|
|
} else {
|
|
debug_f("invalid tunnel %u", tun);
|
|
return -1;
|
|
}
|
|
|
|
if (fd == -1) {
|
|
debug_f("%s open: %s", name, strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
debug_f("%s mode %d fd %d", name, mode, fd);
|
|
|
|
/* Bring interface up if it is not already */
|
|
snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s%d", tunbase, tun);
|
|
if ((sock = socket(PF_UNIX, SOCK_STREAM, 0)) == -1)
|
|
goto failed;
|
|
|
|
if (ioctl(sock, SIOCGIFFLAGS, &ifr) == -1) {
|
|
debug_f("get interface %s flags: %s", ifr.ifr_name,
|
|
strerror(errno));
|
|
goto failed;
|
|
}
|
|
|
|
if (!(ifr.ifr_flags & IFF_UP)) {
|
|
ifr.ifr_flags |= IFF_UP;
|
|
if (ioctl(sock, SIOCSIFFLAGS, &ifr) == -1) {
|
|
debug_f("activate interface %s: %s", ifr.ifr_name,
|
|
strerror(errno));
|
|
goto failed;
|
|
}
|
|
}
|
|
|
|
if (ifname != NULL)
|
|
*ifname = xstrdup(ifr.ifr_name);
|
|
|
|
close(sock);
|
|
return fd;
|
|
|
|
failed:
|
|
if (fd >= 0)
|
|
close(fd);
|
|
if (sock >= 0)
|
|
close(sock);
|
|
return -1;
|
|
#else
|
|
error("Tunnel interfaces are not supported on this platform");
|
|
return (-1);
|
|
#endif
|
|
}
|
|
|
|
void
|
|
sanitise_stdfd(void)
|
|
{
|
|
int nullfd, dupfd;
|
|
|
|
if ((nullfd = dupfd = open(_PATH_DEVNULL, O_RDWR)) == -1) {
|
|
fprintf(stderr, "Couldn't open /dev/null: %s\n",
|
|
strerror(errno));
|
|
exit(1);
|
|
}
|
|
while (++dupfd <= STDERR_FILENO) {
|
|
/* Only populate closed fds. */
|
|
if (fcntl(dupfd, F_GETFL) == -1 && errno == EBADF) {
|
|
if (dup2(nullfd, dupfd) == -1) {
|
|
fprintf(stderr, "dup2: %s\n", strerror(errno));
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
if (nullfd > STDERR_FILENO)
|
|
close(nullfd);
|
|
}
|
|
|
|
char *
|
|
tohex(const void *vp, size_t l)
|
|
{
|
|
const u_char *p = (const u_char *)vp;
|
|
char b[3], *r;
|
|
size_t i, hl;
|
|
|
|
if (l > 65536)
|
|
return xstrdup("tohex: length > 65536");
|
|
|
|
hl = l * 2 + 1;
|
|
r = xcalloc(1, hl);
|
|
for (i = 0; i < l; i++) {
|
|
snprintf(b, sizeof(b), "%02x", p[i]);
|
|
strlcat(r, b, hl);
|
|
}
|
|
return (r);
|
|
}
|
|
|
|
/*
|
|
* Extend string *sp by the specified format. If *sp is not NULL (or empty),
|
|
* then the separator 'sep' will be prepended before the formatted arguments.
|
|
* Extended strings are heap allocated.
|
|
*/
|
|
void
|
|
xextendf(char **sp, const char *sep, const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
char *tmp1, *tmp2;
|
|
|
|
va_start(ap, fmt);
|
|
xvasprintf(&tmp1, fmt, ap);
|
|
va_end(ap);
|
|
|
|
if (*sp == NULL || **sp == '\0') {
|
|
free(*sp);
|
|
*sp = tmp1;
|
|
return;
|
|
}
|
|
xasprintf(&tmp2, "%s%s%s", *sp, sep == NULL ? "" : sep, tmp1);
|
|
free(tmp1);
|
|
free(*sp);
|
|
*sp = tmp2;
|
|
}
|
|
|
|
|
|
u_int64_t
|
|
get_u64(const void *vp)
|
|
{
|
|
const u_char *p = (const u_char *)vp;
|
|
u_int64_t v;
|
|
|
|
v = (u_int64_t)p[0] << 56;
|
|
v |= (u_int64_t)p[1] << 48;
|
|
v |= (u_int64_t)p[2] << 40;
|
|
v |= (u_int64_t)p[3] << 32;
|
|
v |= (u_int64_t)p[4] << 24;
|
|
v |= (u_int64_t)p[5] << 16;
|
|
v |= (u_int64_t)p[6] << 8;
|
|
v |= (u_int64_t)p[7];
|
|
|
|
return (v);
|
|
}
|
|
|
|
u_int32_t
|
|
get_u32(const void *vp)
|
|
{
|
|
const u_char *p = (const u_char *)vp;
|
|
u_int32_t v;
|
|
|
|
v = (u_int32_t)p[0] << 24;
|
|
v |= (u_int32_t)p[1] << 16;
|
|
v |= (u_int32_t)p[2] << 8;
|
|
v |= (u_int32_t)p[3];
|
|
|
|
return (v);
|
|
}
|
|
|
|
u_int32_t
|
|
get_u32_le(const void *vp)
|
|
{
|
|
const u_char *p = (const u_char *)vp;
|
|
u_int32_t v;
|
|
|
|
v = (u_int32_t)p[0];
|
|
v |= (u_int32_t)p[1] << 8;
|
|
v |= (u_int32_t)p[2] << 16;
|
|
v |= (u_int32_t)p[3] << 24;
|
|
|
|
return (v);
|
|
}
|
|
|
|
u_int16_t
|
|
get_u16(const void *vp)
|
|
{
|
|
const u_char *p = (const u_char *)vp;
|
|
u_int16_t v;
|
|
|
|
v = (u_int16_t)p[0] << 8;
|
|
v |= (u_int16_t)p[1];
|
|
|
|
return (v);
|
|
}
|
|
|
|
void
|
|
put_u64(void *vp, u_int64_t v)
|
|
{
|
|
u_char *p = (u_char *)vp;
|
|
|
|
p[0] = (u_char)(v >> 56) & 0xff;
|
|
p[1] = (u_char)(v >> 48) & 0xff;
|
|
p[2] = (u_char)(v >> 40) & 0xff;
|
|
p[3] = (u_char)(v >> 32) & 0xff;
|
|
p[4] = (u_char)(v >> 24) & 0xff;
|
|
p[5] = (u_char)(v >> 16) & 0xff;
|
|
p[6] = (u_char)(v >> 8) & 0xff;
|
|
p[7] = (u_char)v & 0xff;
|
|
}
|
|
|
|
void
|
|
put_u32(void *vp, u_int32_t v)
|
|
{
|
|
u_char *p = (u_char *)vp;
|
|
|
|
p[0] = (u_char)(v >> 24) & 0xff;
|
|
p[1] = (u_char)(v >> 16) & 0xff;
|
|
p[2] = (u_char)(v >> 8) & 0xff;
|
|
p[3] = (u_char)v & 0xff;
|
|
}
|
|
|
|
void
|
|
put_u32_le(void *vp, u_int32_t v)
|
|
{
|
|
u_char *p = (u_char *)vp;
|
|
|
|
p[0] = (u_char)v & 0xff;
|
|
p[1] = (u_char)(v >> 8) & 0xff;
|
|
p[2] = (u_char)(v >> 16) & 0xff;
|
|
p[3] = (u_char)(v >> 24) & 0xff;
|
|
}
|
|
|
|
void
|
|
put_u16(void *vp, u_int16_t v)
|
|
{
|
|
u_char *p = (u_char *)vp;
|
|
|
|
p[0] = (u_char)(v >> 8) & 0xff;
|
|
p[1] = (u_char)v & 0xff;
|
|
}
|
|
|
|
void
|
|
ms_subtract_diff(struct timeval *start, int *ms)
|
|
{
|
|
struct timeval diff, finish;
|
|
|
|
monotime_tv(&finish);
|
|
timersub(&finish, start, &diff);
|
|
*ms -= (diff.tv_sec * 1000) + (diff.tv_usec / 1000);
|
|
}
|
|
|
|
void
|
|
ms_to_timespec(struct timespec *ts, int ms)
|
|
{
|
|
if (ms < 0)
|
|
ms = 0;
|
|
ts->tv_sec = ms / 1000;
|
|
ts->tv_nsec = (ms % 1000) * 1000 * 1000;
|
|
}
|
|
|
|
void
|
|
monotime_ts(struct timespec *ts)
|
|
{
|
|
struct timeval tv;
|
|
#if defined(HAVE_CLOCK_GETTIME) && (defined(CLOCK_BOOTTIME) || \
|
|
defined(CLOCK_MONOTONIC) || defined(CLOCK_REALTIME))
|
|
static int gettime_failed = 0;
|
|
|
|
if (!gettime_failed) {
|
|
# ifdef CLOCK_BOOTTIME
|
|
if (clock_gettime(CLOCK_BOOTTIME, ts) == 0)
|
|
return;
|
|
# endif /* CLOCK_BOOTTIME */
|
|
# ifdef CLOCK_MONOTONIC
|
|
if (clock_gettime(CLOCK_MONOTONIC, ts) == 0)
|
|
return;
|
|
# endif /* CLOCK_MONOTONIC */
|
|
# ifdef CLOCK_REALTIME
|
|
/* Not monotonic, but we're almost out of options here. */
|
|
if (clock_gettime(CLOCK_REALTIME, ts) == 0)
|
|
return;
|
|
# endif /* CLOCK_REALTIME */
|
|
debug3("clock_gettime: %s", strerror(errno));
|
|
gettime_failed = 1;
|
|
}
|
|
#endif /* HAVE_CLOCK_GETTIME && (BOOTTIME || MONOTONIC || REALTIME) */
|
|
gettimeofday(&tv, NULL);
|
|
ts->tv_sec = tv.tv_sec;
|
|
ts->tv_nsec = (long)tv.tv_usec * 1000;
|
|
}
|
|
|
|
void
|
|
monotime_tv(struct timeval *tv)
|
|
{
|
|
struct timespec ts;
|
|
|
|
monotime_ts(&ts);
|
|
tv->tv_sec = ts.tv_sec;
|
|
tv->tv_usec = ts.tv_nsec / 1000;
|
|
}
|
|
|
|
time_t
|
|
monotime(void)
|
|
{
|
|
struct timespec ts;
|
|
|
|
monotime_ts(&ts);
|
|
return ts.tv_sec;
|
|
}
|
|
|
|
double
|
|
monotime_double(void)
|
|
{
|
|
struct timespec ts;
|
|
|
|
monotime_ts(&ts);
|
|
return ts.tv_sec + ((double)ts.tv_nsec / 1000000000);
|
|
}
|
|
|
|
void
|
|
bandwidth_limit_init(struct bwlimit *bw, u_int64_t kbps, size_t buflen)
|
|
{
|
|
bw->buflen = buflen;
|
|
bw->rate = kbps;
|
|
bw->thresh = buflen;
|
|
bw->lamt = 0;
|
|
timerclear(&bw->bwstart);
|
|
timerclear(&bw->bwend);
|
|
}
|
|
|
|
/* Callback from read/write loop to insert bandwidth-limiting delays */
|
|
void
|
|
bandwidth_limit(struct bwlimit *bw, size_t read_len)
|
|
{
|
|
u_int64_t waitlen;
|
|
struct timespec ts, rm;
|
|
|
|
bw->lamt += read_len;
|
|
if (!timerisset(&bw->bwstart)) {
|
|
monotime_tv(&bw->bwstart);
|
|
return;
|
|
}
|
|
if (bw->lamt < bw->thresh)
|
|
return;
|
|
|
|
monotime_tv(&bw->bwend);
|
|
timersub(&bw->bwend, &bw->bwstart, &bw->bwend);
|
|
if (!timerisset(&bw->bwend))
|
|
return;
|
|
|
|
bw->lamt *= 8;
|
|
waitlen = (double)1000000L * bw->lamt / bw->rate;
|
|
|
|
bw->bwstart.tv_sec = waitlen / 1000000L;
|
|
bw->bwstart.tv_usec = waitlen % 1000000L;
|
|
|
|
if (timercmp(&bw->bwstart, &bw->bwend, >)) {
|
|
timersub(&bw->bwstart, &bw->bwend, &bw->bwend);
|
|
|
|
/* Adjust the wait time */
|
|
if (bw->bwend.tv_sec) {
|
|
bw->thresh /= 2;
|
|
if (bw->thresh < bw->buflen / 4)
|
|
bw->thresh = bw->buflen / 4;
|
|
} else if (bw->bwend.tv_usec < 10000) {
|
|
bw->thresh *= 2;
|
|
if (bw->thresh > bw->buflen * 8)
|
|
bw->thresh = bw->buflen * 8;
|
|
}
|
|
|
|
TIMEVAL_TO_TIMESPEC(&bw->bwend, &ts);
|
|
while (nanosleep(&ts, &rm) == -1) {
|
|
if (errno != EINTR)
|
|
break;
|
|
ts = rm;
|
|
}
|
|
}
|
|
|
|
bw->lamt = 0;
|
|
monotime_tv(&bw->bwstart);
|
|
}
|
|
|
|
/* Make a template filename for mk[sd]temp() */
|
|
void
|
|
mktemp_proto(char *s, size_t len)
|
|
{
|
|
const char *tmpdir;
|
|
int r;
|
|
|
|
if ((tmpdir = getenv("TMPDIR")) != NULL) {
|
|
r = snprintf(s, len, "%s/ssh-XXXXXXXXXXXX", tmpdir);
|
|
if (r > 0 && (size_t)r < len)
|
|
return;
|
|
}
|
|
r = snprintf(s, len, "/tmp/ssh-XXXXXXXXXXXX");
|
|
if (r < 0 || (size_t)r >= len)
|
|
fatal_f("template string too short");
|
|
}
|
|
|
|
static const struct {
|
|
const char *name;
|
|
int value;
|
|
} ipqos[] = {
|
|
{ "none", INT_MAX }, /* can't use 0 here; that's CS0 */
|
|
{ "af11", IPTOS_DSCP_AF11 },
|
|
{ "af12", IPTOS_DSCP_AF12 },
|
|
{ "af13", IPTOS_DSCP_AF13 },
|
|
{ "af21", IPTOS_DSCP_AF21 },
|
|
{ "af22", IPTOS_DSCP_AF22 },
|
|
{ "af23", IPTOS_DSCP_AF23 },
|
|
{ "af31", IPTOS_DSCP_AF31 },
|
|
{ "af32", IPTOS_DSCP_AF32 },
|
|
{ "af33", IPTOS_DSCP_AF33 },
|
|
{ "af41", IPTOS_DSCP_AF41 },
|
|
{ "af42", IPTOS_DSCP_AF42 },
|
|
{ "af43", IPTOS_DSCP_AF43 },
|
|
{ "cs0", IPTOS_DSCP_CS0 },
|
|
{ "cs1", IPTOS_DSCP_CS1 },
|
|
{ "cs2", IPTOS_DSCP_CS2 },
|
|
{ "cs3", IPTOS_DSCP_CS3 },
|
|
{ "cs4", IPTOS_DSCP_CS4 },
|
|
{ "cs5", IPTOS_DSCP_CS5 },
|
|
{ "cs6", IPTOS_DSCP_CS6 },
|
|
{ "cs7", IPTOS_DSCP_CS7 },
|
|
{ "ef", IPTOS_DSCP_EF },
|
|
{ "le", IPTOS_DSCP_LE },
|
|
{ "lowdelay", IPTOS_LOWDELAY },
|
|
{ "throughput", IPTOS_THROUGHPUT },
|
|
{ "reliability", IPTOS_RELIABILITY },
|
|
{ NULL, -1 }
|
|
};
|
|
|
|
int
|
|
parse_ipqos(const char *cp)
|
|
{
|
|
const char *errstr;
|
|
u_int i;
|
|
int val;
|
|
|
|
if (cp == NULL)
|
|
return -1;
|
|
for (i = 0; ipqos[i].name != NULL; i++) {
|
|
if (strcasecmp(cp, ipqos[i].name) == 0)
|
|
return ipqos[i].value;
|
|
}
|
|
/* Try parsing as an integer */
|
|
val = (int)strtonum(cp, 0, 255, &errstr);
|
|
if (errstr)
|
|
return -1;
|
|
return val;
|
|
}
|
|
|
|
const char *
|
|
iptos2str(int iptos)
|
|
{
|
|
int i;
|
|
static char iptos_str[sizeof "0xff"];
|
|
|
|
for (i = 0; ipqos[i].name != NULL; i++) {
|
|
if (ipqos[i].value == iptos)
|
|
return ipqos[i].name;
|
|
}
|
|
snprintf(iptos_str, sizeof iptos_str, "0x%02x", iptos);
|
|
return iptos_str;
|
|
}
|
|
|
|
void
|
|
lowercase(char *s)
|
|
{
|
|
for (; *s; s++)
|
|
*s = tolower((u_char)*s);
|
|
}
|
|
|
|
int
|
|
unix_listener(const char *path, int backlog, int unlink_first)
|
|
{
|
|
struct sockaddr_un sunaddr;
|
|
int saved_errno, sock;
|
|
|
|
memset(&sunaddr, 0, sizeof(sunaddr));
|
|
sunaddr.sun_family = AF_UNIX;
|
|
if (strlcpy(sunaddr.sun_path, path,
|
|
sizeof(sunaddr.sun_path)) >= sizeof(sunaddr.sun_path)) {
|
|
error_f("path \"%s\" too long for Unix domain socket", path);
|
|
errno = ENAMETOOLONG;
|
|
return -1;
|
|
}
|
|
|
|
sock = socket(PF_UNIX, SOCK_STREAM, 0);
|
|
if (sock == -1) {
|
|
saved_errno = errno;
|
|
error_f("socket: %.100s", strerror(errno));
|
|
errno = saved_errno;
|
|
return -1;
|
|
}
|
|
if (unlink_first == 1) {
|
|
if (unlink(path) != 0 && errno != ENOENT)
|
|
error("unlink(%s): %.100s", path, strerror(errno));
|
|
}
|
|
if (bind(sock, (struct sockaddr *)&sunaddr, sizeof(sunaddr)) == -1) {
|
|
saved_errno = errno;
|
|
error_f("cannot bind to path %s: %s", path, strerror(errno));
|
|
close(sock);
|
|
errno = saved_errno;
|
|
return -1;
|
|
}
|
|
if (listen(sock, backlog) == -1) {
|
|
saved_errno = errno;
|
|
error_f("cannot listen on path %s: %s", path, strerror(errno));
|
|
close(sock);
|
|
unlink(path);
|
|
errno = saved_errno;
|
|
return -1;
|
|
}
|
|
return sock;
|
|
}
|
|
|
|
void
|
|
sock_set_v6only(int s)
|
|
{
|
|
#if defined(IPV6_V6ONLY) && !defined(__OpenBSD__)
|
|
int on = 1;
|
|
|
|
debug3("%s: set socket %d IPV6_V6ONLY", __func__, s);
|
|
if (setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) == -1)
|
|
error("setsockopt IPV6_V6ONLY: %s", strerror(errno));
|
|
#endif
|
|
}
|
|
|
|
/*
|
|
* Compares two strings that maybe be NULL. Returns non-zero if strings
|
|
* are both NULL or are identical, returns zero otherwise.
|
|
*/
|
|
static int
|
|
strcmp_maybe_null(const char *a, const char *b)
|
|
{
|
|
if ((a == NULL && b != NULL) || (a != NULL && b == NULL))
|
|
return 0;
|
|
if (a != NULL && strcmp(a, b) != 0)
|
|
return 0;
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
* Compare two forwards, returning non-zero if they are identical or
|
|
* zero otherwise.
|
|
*/
|
|
int
|
|
forward_equals(const struct Forward *a, const struct Forward *b)
|
|
{
|
|
if (strcmp_maybe_null(a->listen_host, b->listen_host) == 0)
|
|
return 0;
|
|
if (a->listen_port != b->listen_port)
|
|
return 0;
|
|
if (strcmp_maybe_null(a->listen_path, b->listen_path) == 0)
|
|
return 0;
|
|
if (strcmp_maybe_null(a->connect_host, b->connect_host) == 0)
|
|
return 0;
|
|
if (a->connect_port != b->connect_port)
|
|
return 0;
|
|
if (strcmp_maybe_null(a->connect_path, b->connect_path) == 0)
|
|
return 0;
|
|
/* allocated_port and handle are not checked */
|
|
return 1;
|
|
}
|
|
|
|
/* returns port number, FWD_PERMIT_ANY_PORT or -1 on error */
|
|
int
|
|
permitopen_port(const char *p)
|
|
{
|
|
int port;
|
|
|
|
if (strcmp(p, "*") == 0)
|
|
return FWD_PERMIT_ANY_PORT;
|
|
if ((port = a2port(p)) > 0)
|
|
return port;
|
|
return -1;
|
|
}
|
|
|
|
/* returns 1 if process is already daemonized, 0 otherwise */
|
|
int
|
|
daemonized(void)
|
|
{
|
|
int fd;
|
|
|
|
if ((fd = open(_PATH_TTY, O_RDONLY | O_NOCTTY)) >= 0) {
|
|
close(fd);
|
|
return 0; /* have controlling terminal */
|
|
}
|
|
if (getppid() != 1)
|
|
return 0; /* parent is not init */
|
|
if (getsid(0) != getpid())
|
|
return 0; /* not session leader */
|
|
debug3("already daemonized");
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
* Splits 's' into an argument vector. Handles quoted string and basic
|
|
* escape characters (\\, \", \'). Caller must free the argument vector
|
|
* and its members.
|
|
*/
|
|
int
|
|
argv_split(const char *s, int *argcp, char ***argvp, int terminate_on_comment)
|
|
{
|
|
int r = SSH_ERR_INTERNAL_ERROR;
|
|
int argc = 0, quote, i, j;
|
|
char *arg, **argv = xcalloc(1, sizeof(*argv));
|
|
|
|
*argvp = NULL;
|
|
*argcp = 0;
|
|
|
|
for (i = 0; s[i] != '\0'; i++) {
|
|
/* Skip leading whitespace */
|
|
if (s[i] == ' ' || s[i] == '\t')
|
|
continue;
|
|
if (terminate_on_comment && s[i] == '#')
|
|
break;
|
|
/* Start of a token */
|
|
quote = 0;
|
|
|
|
argv = xreallocarray(argv, (argc + 2), sizeof(*argv));
|
|
arg = argv[argc++] = xcalloc(1, strlen(s + i) + 1);
|
|
argv[argc] = NULL;
|
|
|
|
/* Copy the token in, removing escapes */
|
|
for (j = 0; s[i] != '\0'; i++) {
|
|
if (s[i] == '\\') {
|
|
if (s[i + 1] == '\'' ||
|
|
s[i + 1] == '\"' ||
|
|
s[i + 1] == '\\' ||
|
|
(quote == 0 && s[i + 1] == ' ')) {
|
|
i++; /* Skip '\' */
|
|
arg[j++] = s[i];
|
|
} else {
|
|
/* Unrecognised escape */
|
|
arg[j++] = s[i];
|
|
}
|
|
} else if (quote == 0 && (s[i] == ' ' || s[i] == '\t'))
|
|
break; /* done */
|
|
else if (quote == 0 && (s[i] == '\"' || s[i] == '\''))
|
|
quote = s[i]; /* quote start */
|
|
else if (quote != 0 && s[i] == quote)
|
|
quote = 0; /* quote end */
|
|
else
|
|
arg[j++] = s[i];
|
|
}
|
|
if (s[i] == '\0') {
|
|
if (quote != 0) {
|
|
/* Ran out of string looking for close quote */
|
|
r = SSH_ERR_INVALID_FORMAT;
|
|
goto out;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
/* Success */
|
|
*argcp = argc;
|
|
*argvp = argv;
|
|
argc = 0;
|
|
argv = NULL;
|
|
r = 0;
|
|
out:
|
|
if (argc != 0 && argv != NULL) {
|
|
for (i = 0; i < argc; i++)
|
|
free(argv[i]);
|
|
free(argv);
|
|
}
|
|
return r;
|
|
}
|
|
|
|
/*
|
|
* Reassemble an argument vector into a string, quoting and escaping as
|
|
* necessary. Caller must free returned string.
|
|
*/
|
|
char *
|
|
argv_assemble(int argc, char **argv)
|
|
{
|
|
int i, j, ws, r;
|
|
char c, *ret;
|
|
struct sshbuf *buf, *arg;
|
|
|
|
if ((buf = sshbuf_new()) == NULL || (arg = sshbuf_new()) == NULL)
|
|
fatal_f("sshbuf_new failed");
|
|
|
|
for (i = 0; i < argc; i++) {
|
|
ws = 0;
|
|
sshbuf_reset(arg);
|
|
for (j = 0; argv[i][j] != '\0'; j++) {
|
|
r = 0;
|
|
c = argv[i][j];
|
|
switch (c) {
|
|
case ' ':
|
|
case '\t':
|
|
ws = 1;
|
|
r = sshbuf_put_u8(arg, c);
|
|
break;
|
|
case '\\':
|
|
case '\'':
|
|
case '"':
|
|
if ((r = sshbuf_put_u8(arg, '\\')) != 0)
|
|
break;
|
|
/* FALLTHROUGH */
|
|
default:
|
|
r = sshbuf_put_u8(arg, c);
|
|
break;
|
|
}
|
|
if (r != 0)
|
|
fatal_fr(r, "sshbuf_put_u8");
|
|
}
|
|
if ((i != 0 && (r = sshbuf_put_u8(buf, ' ')) != 0) ||
|
|
(ws != 0 && (r = sshbuf_put_u8(buf, '"')) != 0) ||
|
|
(r = sshbuf_putb(buf, arg)) != 0 ||
|
|
(ws != 0 && (r = sshbuf_put_u8(buf, '"')) != 0))
|
|
fatal_fr(r, "assemble");
|
|
}
|
|
if ((ret = malloc(sshbuf_len(buf) + 1)) == NULL)
|
|
fatal_f("malloc failed");
|
|
memcpy(ret, sshbuf_ptr(buf), sshbuf_len(buf));
|
|
ret[sshbuf_len(buf)] = '\0';
|
|
sshbuf_free(buf);
|
|
sshbuf_free(arg);
|
|
return ret;
|
|
}
|
|
|
|
char *
|
|
argv_next(int *argcp, char ***argvp)
|
|
{
|
|
char *ret = (*argvp)[0];
|
|
|
|
if (*argcp > 0 && ret != NULL) {
|
|
(*argcp)--;
|
|
(*argvp)++;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
void
|
|
argv_consume(int *argcp)
|
|
{
|
|
*argcp = 0;
|
|
}
|
|
|
|
void
|
|
argv_free(char **av, int ac)
|
|
{
|
|
int i;
|
|
|
|
if (av == NULL)
|
|
return;
|
|
for (i = 0; i < ac; i++)
|
|
free(av[i]);
|
|
free(av);
|
|
}
|
|
|
|
/* Returns 0 if pid exited cleanly, non-zero otherwise */
|
|
int
|
|
exited_cleanly(pid_t pid, const char *tag, const char *cmd, int quiet)
|
|
{
|
|
int status;
|
|
|
|
while (waitpid(pid, &status, 0) == -1) {
|
|
if (errno != EINTR) {
|
|
error("%s waitpid: %s", tag, strerror(errno));
|
|
return -1;
|
|
}
|
|
}
|
|
if (WIFSIGNALED(status)) {
|
|
error("%s %s exited on signal %d", tag, cmd, WTERMSIG(status));
|
|
return -1;
|
|
} else if (WEXITSTATUS(status) != 0) {
|
|
do_log2(quiet ? SYSLOG_LEVEL_DEBUG1 : SYSLOG_LEVEL_INFO,
|
|
"%s %s failed, status %d", tag, cmd, WEXITSTATUS(status));
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Check a given path for security. This is defined as all components
|
|
* of the path to the file must be owned by either the owner of
|
|
* of the file or root and no directories must be group or world writable.
|
|
*
|
|
* XXX Should any specific check be done for sym links ?
|
|
*
|
|
* Takes a file name, its stat information (preferably from fstat() to
|
|
* avoid races), the uid of the expected owner, their home directory and an
|
|
* error buffer plus max size as arguments.
|
|
*
|
|
* Returns 0 on success and -1 on failure
|
|
*/
|
|
int
|
|
safe_path(const char *name, struct stat *stp, const char *pw_dir,
|
|
uid_t uid, char *err, size_t errlen)
|
|
{
|
|
char buf[PATH_MAX], homedir[PATH_MAX];
|
|
char *cp;
|
|
int comparehome = 0;
|
|
struct stat st;
|
|
|
|
if (realpath(name, buf) == NULL) {
|
|
snprintf(err, errlen, "realpath %s failed: %s", name,
|
|
strerror(errno));
|
|
return -1;
|
|
}
|
|
if (pw_dir != NULL && realpath(pw_dir, homedir) != NULL)
|
|
comparehome = 1;
|
|
|
|
if (!S_ISREG(stp->st_mode)) {
|
|
snprintf(err, errlen, "%s is not a regular file", buf);
|
|
return -1;
|
|
}
|
|
if ((!platform_sys_dir_uid(stp->st_uid) && stp->st_uid != uid) ||
|
|
(stp->st_mode & 022) != 0) {
|
|
snprintf(err, errlen, "bad ownership or modes for file %s",
|
|
buf);
|
|
return -1;
|
|
}
|
|
|
|
/* for each component of the canonical path, walking upwards */
|
|
for (;;) {
|
|
if ((cp = dirname(buf)) == NULL) {
|
|
snprintf(err, errlen, "dirname() failed");
|
|
return -1;
|
|
}
|
|
strlcpy(buf, cp, sizeof(buf));
|
|
|
|
if (stat(buf, &st) == -1 ||
|
|
(!platform_sys_dir_uid(st.st_uid) && st.st_uid != uid) ||
|
|
(st.st_mode & 022) != 0) {
|
|
snprintf(err, errlen,
|
|
"bad ownership or modes for directory %s", buf);
|
|
return -1;
|
|
}
|
|
|
|
/* If are past the homedir then we can stop */
|
|
if (comparehome && strcmp(homedir, buf) == 0)
|
|
break;
|
|
|
|
/*
|
|
* dirname should always complete with a "/" path,
|
|
* but we can be paranoid and check for "." too
|
|
*/
|
|
if ((strcmp("/", buf) == 0) || (strcmp(".", buf) == 0))
|
|
break;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Version of safe_path() that accepts an open file descriptor to
|
|
* avoid races.
|
|
*
|
|
* Returns 0 on success and -1 on failure
|
|
*/
|
|
int
|
|
safe_path_fd(int fd, const char *file, struct passwd *pw,
|
|
char *err, size_t errlen)
|
|
{
|
|
struct stat st;
|
|
|
|
/* check the open file to avoid races */
|
|
if (fstat(fd, &st) == -1) {
|
|
snprintf(err, errlen, "cannot stat file %s: %s",
|
|
file, strerror(errno));
|
|
return -1;
|
|
}
|
|
return safe_path(file, &st, pw->pw_dir, pw->pw_uid, err, errlen);
|
|
}
|
|
|
|
/*
|
|
* Sets the value of the given variable in the environment. If the variable
|
|
* already exists, its value is overridden.
|
|
*/
|
|
void
|
|
child_set_env(char ***envp, u_int *envsizep, const char *name,
|
|
const char *value)
|
|
{
|
|
char **env;
|
|
u_int envsize;
|
|
u_int i, namelen;
|
|
|
|
if (strchr(name, '=') != NULL) {
|
|
error("Invalid environment variable \"%.100s\"", name);
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* If we're passed an uninitialized list, allocate a single null
|
|
* entry before continuing.
|
|
*/
|
|
if ((*envp == NULL) != (*envsizep == 0))
|
|
fatal_f("environment size mismatch");
|
|
if (*envp == NULL && *envsizep == 0) {
|
|
*envp = xmalloc(sizeof(char *));
|
|
*envp[0] = NULL;
|
|
*envsizep = 1;
|
|
}
|
|
|
|
/*
|
|
* Find the slot where the value should be stored. If the variable
|
|
* already exists, we reuse the slot; otherwise we append a new slot
|
|
* at the end of the array, expanding if necessary.
|
|
*/
|
|
env = *envp;
|
|
namelen = strlen(name);
|
|
for (i = 0; env[i]; i++)
|
|
if (strncmp(env[i], name, namelen) == 0 && env[i][namelen] == '=')
|
|
break;
|
|
if (env[i]) {
|
|
/* Reuse the slot. */
|
|
free(env[i]);
|
|
} else {
|
|
/* New variable. Expand if necessary. */
|
|
envsize = *envsizep;
|
|
if (i >= envsize - 1) {
|
|
if (envsize >= 1000)
|
|
fatal("child_set_env: too many env vars");
|
|
envsize += 50;
|
|
env = (*envp) = xreallocarray(env, envsize, sizeof(char *));
|
|
*envsizep = envsize;
|
|
}
|
|
/* Need to set the NULL pointer at end of array beyond the new slot. */
|
|
env[i + 1] = NULL;
|
|
}
|
|
|
|
/* Allocate space and format the variable in the appropriate slot. */
|
|
/* XXX xasprintf */
|
|
env[i] = xmalloc(strlen(name) + 1 + strlen(value) + 1);
|
|
snprintf(env[i], strlen(name) + 1 + strlen(value) + 1, "%s=%s", name, value);
|
|
}
|
|
|
|
/*
|
|
* Check and optionally lowercase a domain name, also removes trailing '.'
|
|
* Returns 1 on success and 0 on failure, storing an error message in errstr.
|
|
*/
|
|
int
|
|
valid_domain(char *name, int makelower, const char **errstr)
|
|
{
|
|
size_t i, l = strlen(name);
|
|
u_char c, last = '\0';
|
|
static char errbuf[256];
|
|
|
|
if (l == 0) {
|
|
strlcpy(errbuf, "empty domain name", sizeof(errbuf));
|
|
goto bad;
|
|
}
|
|
if (!isalpha((u_char)name[0]) && !isdigit((u_char)name[0])) {
|
|
snprintf(errbuf, sizeof(errbuf), "domain name \"%.100s\" "
|
|
"starts with invalid character", name);
|
|
goto bad;
|
|
}
|
|
for (i = 0; i < l; i++) {
|
|
c = tolower((u_char)name[i]);
|
|
if (makelower)
|
|
name[i] = (char)c;
|
|
if (last == '.' && c == '.') {
|
|
snprintf(errbuf, sizeof(errbuf), "domain name "
|
|
"\"%.100s\" contains consecutive separators", name);
|
|
goto bad;
|
|
}
|
|
if (c != '.' && c != '-' && !isalnum(c) &&
|
|
c != '_') /* technically invalid, but common */ {
|
|
snprintf(errbuf, sizeof(errbuf), "domain name "
|
|
"\"%.100s\" contains invalid characters", name);
|
|
goto bad;
|
|
}
|
|
last = c;
|
|
}
|
|
if (name[l - 1] == '.')
|
|
name[l - 1] = '\0';
|
|
if (errstr != NULL)
|
|
*errstr = NULL;
|
|
return 1;
|
|
bad:
|
|
if (errstr != NULL)
|
|
*errstr = errbuf;
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Verify that a environment variable name (not including initial '$') is
|
|
* valid; consisting of one or more alphanumeric or underscore characters only.
|
|
* Returns 1 on valid, 0 otherwise.
|
|
*/
|
|
int
|
|
valid_env_name(const char *name)
|
|
{
|
|
const char *cp;
|
|
|
|
if (name[0] == '\0')
|
|
return 0;
|
|
for (cp = name; *cp != '\0'; cp++) {
|
|
if (!isalnum((u_char)*cp) && *cp != '_')
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
const char *
|
|
atoi_err(const char *nptr, int *val)
|
|
{
|
|
const char *errstr = NULL;
|
|
|
|
if (nptr == NULL || *nptr == '\0')
|
|
return "missing";
|
|
*val = strtonum(nptr, 0, INT_MAX, &errstr);
|
|
return errstr;
|
|
}
|
|
|
|
int
|
|
parse_absolute_time(const char *s, uint64_t *tp)
|
|
{
|
|
struct tm tm;
|
|
time_t tt;
|
|
char buf[32], *fmt;
|
|
const char *cp;
|
|
size_t l;
|
|
int is_utc = 0;
|
|
|
|
*tp = 0;
|
|
|
|
l = strlen(s);
|
|
if (l > 1 && strcasecmp(s + l - 1, "Z") == 0) {
|
|
is_utc = 1;
|
|
l--;
|
|
} else if (l > 3 && strcasecmp(s + l - 3, "UTC") == 0) {
|
|
is_utc = 1;
|
|
l -= 3;
|
|
}
|
|
/*
|
|
* POSIX strptime says "The application shall ensure that there
|
|
* is white-space or other non-alphanumeric characters between
|
|
* any two conversion specifications" so arrange things this way.
|
|
*/
|
|
switch (l) {
|
|
case 8: /* YYYYMMDD */
|
|
fmt = "%Y-%m-%d";
|
|
snprintf(buf, sizeof(buf), "%.4s-%.2s-%.2s", s, s + 4, s + 6);
|
|
break;
|
|
case 12: /* YYYYMMDDHHMM */
|
|
fmt = "%Y-%m-%dT%H:%M";
|
|
snprintf(buf, sizeof(buf), "%.4s-%.2s-%.2sT%.2s:%.2s",
|
|
s, s + 4, s + 6, s + 8, s + 10);
|
|
break;
|
|
case 14: /* YYYYMMDDHHMMSS */
|
|
fmt = "%Y-%m-%dT%H:%M:%S";
|
|
snprintf(buf, sizeof(buf), "%.4s-%.2s-%.2sT%.2s:%.2s:%.2s",
|
|
s, s + 4, s + 6, s + 8, s + 10, s + 12);
|
|
break;
|
|
default:
|
|
return SSH_ERR_INVALID_FORMAT;
|
|
}
|
|
|
|
memset(&tm, 0, sizeof(tm));
|
|
if ((cp = strptime(buf, fmt, &tm)) == NULL || *cp != '\0')
|
|
return SSH_ERR_INVALID_FORMAT;
|
|
if (is_utc) {
|
|
if ((tt = timegm(&tm)) < 0)
|
|
return SSH_ERR_INVALID_FORMAT;
|
|
} else {
|
|
if ((tt = mktime(&tm)) < 0)
|
|
return SSH_ERR_INVALID_FORMAT;
|
|
}
|
|
/* success */
|
|
*tp = (uint64_t)tt;
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
format_absolute_time(uint64_t t, char *buf, size_t len)
|
|
{
|
|
time_t tt = t > SSH_TIME_T_MAX ? SSH_TIME_T_MAX : t;
|
|
struct tm tm;
|
|
|
|
localtime_r(&tt, &tm);
|
|
strftime(buf, len, "%Y-%m-%dT%H:%M:%S", &tm);
|
|
}
|
|
|
|
/*
|
|
* Parse a "pattern=interval" clause (e.g. a ChannelTimeout).
|
|
* Returns 0 on success or non-zero on failure.
|
|
* Caller must free *typep.
|
|
*/
|
|
int
|
|
parse_pattern_interval(const char *s, char **typep, int *secsp)
|
|
{
|
|
char *cp, *sdup;
|
|
int secs;
|
|
|
|
if (typep != NULL)
|
|
*typep = NULL;
|
|
if (secsp != NULL)
|
|
*secsp = 0;
|
|
if (s == NULL)
|
|
return -1;
|
|
sdup = xstrdup(s);
|
|
|
|
if ((cp = strchr(sdup, '=')) == NULL || cp == sdup) {
|
|
free(sdup);
|
|
return -1;
|
|
}
|
|
*cp++ = '\0';
|
|
if ((secs = convtime(cp)) < 0) {
|
|
free(sdup);
|
|
return -1;
|
|
}
|
|
/* success */
|
|
if (typep != NULL)
|
|
*typep = xstrdup(sdup);
|
|
if (secsp != NULL)
|
|
*secsp = secs;
|
|
free(sdup);
|
|
return 0;
|
|
}
|
|
|
|
/* check if path is absolute */
|
|
int
|
|
path_absolute(const char *path)
|
|
{
|
|
return (*path == '/') ? 1 : 0;
|
|
}
|
|
|
|
void
|
|
skip_space(char **cpp)
|
|
{
|
|
char *cp;
|
|
|
|
for (cp = *cpp; *cp == ' ' || *cp == '\t'; cp++)
|
|
;
|
|
*cpp = cp;
|
|
}
|
|
|
|
/* authorized_key-style options parsing helpers */
|
|
|
|
/*
|
|
* Match flag 'opt' in *optsp, and if allow_negate is set then also match
|
|
* 'no-opt'. Returns -1 if option not matched, 1 if option matches or 0
|
|
* if negated option matches.
|
|
* If the option or negated option matches, then *optsp is updated to
|
|
* point to the first character after the option.
|
|
*/
|
|
int
|
|
opt_flag(const char *opt, int allow_negate, const char **optsp)
|
|
{
|
|
size_t opt_len = strlen(opt);
|
|
const char *opts = *optsp;
|
|
int negate = 0;
|
|
|
|
if (allow_negate && strncasecmp(opts, "no-", 3) == 0) {
|
|
opts += 3;
|
|
negate = 1;
|
|
}
|
|
if (strncasecmp(opts, opt, opt_len) == 0) {
|
|
*optsp = opts + opt_len;
|
|
return negate ? 0 : 1;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
char *
|
|
opt_dequote(const char **sp, const char **errstrp)
|
|
{
|
|
const char *s = *sp;
|
|
char *ret;
|
|
size_t i;
|
|
|
|
*errstrp = NULL;
|
|
if (*s != '"') {
|
|
*errstrp = "missing start quote";
|
|
return NULL;
|
|
}
|
|
s++;
|
|
if ((ret = malloc(strlen((s)) + 1)) == NULL) {
|
|
*errstrp = "memory allocation failed";
|
|
return NULL;
|
|
}
|
|
for (i = 0; *s != '\0' && *s != '"';) {
|
|
if (s[0] == '\\' && s[1] == '"')
|
|
s++;
|
|
ret[i++] = *s++;
|
|
}
|
|
if (*s == '\0') {
|
|
*errstrp = "missing end quote";
|
|
free(ret);
|
|
return NULL;
|
|
}
|
|
ret[i] = '\0';
|
|
s++;
|
|
*sp = s;
|
|
return ret;
|
|
}
|
|
|
|
int
|
|
opt_match(const char **opts, const char *term)
|
|
{
|
|
if (strncasecmp((*opts), term, strlen(term)) == 0 &&
|
|
(*opts)[strlen(term)] == '=') {
|
|
*opts += strlen(term) + 1;
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
opt_array_append2(const char *file, const int line, const char *directive,
|
|
char ***array, int **iarray, u_int *lp, const char *s, int i)
|
|
{
|
|
|
|
if (*lp >= INT_MAX)
|
|
fatal("%s line %d: Too many %s entries", file, line, directive);
|
|
|
|
if (iarray != NULL) {
|
|
*iarray = xrecallocarray(*iarray, *lp, *lp + 1,
|
|
sizeof(**iarray));
|
|
(*iarray)[*lp] = i;
|
|
}
|
|
|
|
*array = xrecallocarray(*array, *lp, *lp + 1, sizeof(**array));
|
|
(*array)[*lp] = xstrdup(s);
|
|
(*lp)++;
|
|
}
|
|
|
|
void
|
|
opt_array_append(const char *file, const int line, const char *directive,
|
|
char ***array, u_int *lp, const char *s)
|
|
{
|
|
opt_array_append2(file, line, directive, array, NULL, lp, s, 0);
|
|
}
|
|
|
|
void
|
|
opt_array_free2(char **array, int **iarray, u_int l)
|
|
{
|
|
u_int i;
|
|
|
|
if (array == NULL || l == 0)
|
|
return;
|
|
for (i = 0; i < l; i++)
|
|
free(array[i]);
|
|
free(array);
|
|
free(iarray);
|
|
}
|
|
|
|
sshsig_t
|
|
ssh_signal(int signum, sshsig_t handler)
|
|
{
|
|
struct sigaction sa, osa;
|
|
|
|
/* mask all other signals while in handler */
|
|
memset(&sa, 0, sizeof(sa));
|
|
sa.sa_handler = handler;
|
|
sigfillset(&sa.sa_mask);
|
|
#if defined(SA_RESTART) && !defined(NO_SA_RESTART)
|
|
if (signum != SIGALRM)
|
|
sa.sa_flags = SA_RESTART;
|
|
#endif
|
|
if (sigaction(signum, &sa, &osa) == -1) {
|
|
debug3("sigaction(%s): %s", strsignal(signum), strerror(errno));
|
|
return SIG_ERR;
|
|
}
|
|
return osa.sa_handler;
|
|
}
|
|
|
|
int
|
|
stdfd_devnull(int do_stdin, int do_stdout, int do_stderr)
|
|
{
|
|
int devnull, ret = 0;
|
|
|
|
if ((devnull = open(_PATH_DEVNULL, O_RDWR)) == -1) {
|
|
error_f("open %s: %s", _PATH_DEVNULL,
|
|
strerror(errno));
|
|
return -1;
|
|
}
|
|
if ((do_stdin && dup2(devnull, STDIN_FILENO) == -1) ||
|
|
(do_stdout && dup2(devnull, STDOUT_FILENO) == -1) ||
|
|
(do_stderr && dup2(devnull, STDERR_FILENO) == -1)) {
|
|
error_f("dup2: %s", strerror(errno));
|
|
ret = -1;
|
|
}
|
|
if (devnull > STDERR_FILENO)
|
|
close(devnull);
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Runs command in a subprocess with a minimal environment.
|
|
* Returns pid on success, 0 on failure.
|
|
* The child stdout and stderr maybe captured, left attached or sent to
|
|
* /dev/null depending on the contents of flags.
|
|
* "tag" is prepended to log messages.
|
|
* NB. "command" is only used for logging; the actual command executed is
|
|
* av[0].
|
|
*/
|
|
pid_t
|
|
subprocess(const char *tag, const char *command,
|
|
int ac, char **av, FILE **child, u_int flags,
|
|
struct passwd *pw, privdrop_fn *drop_privs, privrestore_fn *restore_privs)
|
|
{
|
|
FILE *f = NULL;
|
|
struct stat st;
|
|
int fd, devnull, p[2], i;
|
|
pid_t pid;
|
|
char *cp, errmsg[512];
|
|
u_int nenv = 0;
|
|
char **env = NULL;
|
|
|
|
/* If dropping privs, then must specify user and restore function */
|
|
if (drop_privs != NULL && (pw == NULL || restore_privs == NULL)) {
|
|
error("%s: inconsistent arguments", tag); /* XXX fatal? */
|
|
return 0;
|
|
}
|
|
if (pw == NULL && (pw = getpwuid(getuid())) == NULL) {
|
|
error("%s: no user for current uid", tag);
|
|
return 0;
|
|
}
|
|
if (child != NULL)
|
|
*child = NULL;
|
|
|
|
debug3_f("%s command \"%s\" running as %s (flags 0x%x)",
|
|
tag, command, pw->pw_name, flags);
|
|
|
|
/* Check consistency */
|
|
if ((flags & SSH_SUBPROCESS_STDOUT_DISCARD) != 0 &&
|
|
(flags & SSH_SUBPROCESS_STDOUT_CAPTURE) != 0) {
|
|
error_f("inconsistent flags");
|
|
return 0;
|
|
}
|
|
if (((flags & SSH_SUBPROCESS_STDOUT_CAPTURE) == 0) != (child == NULL)) {
|
|
error_f("inconsistent flags/output");
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* If executing an explicit binary, then verify the it exists
|
|
* and appears safe-ish to execute
|
|
*/
|
|
if (!path_absolute(av[0])) {
|
|
error("%s path is not absolute", tag);
|
|
return 0;
|
|
}
|
|
if (drop_privs != NULL)
|
|
drop_privs(pw);
|
|
if (stat(av[0], &st) == -1) {
|
|
error("Could not stat %s \"%s\": %s", tag,
|
|
av[0], strerror(errno));
|
|
goto restore_return;
|
|
}
|
|
if ((flags & SSH_SUBPROCESS_UNSAFE_PATH) == 0 &&
|
|
safe_path(av[0], &st, NULL, 0, errmsg, sizeof(errmsg)) != 0) {
|
|
error("Unsafe %s \"%s\": %s", tag, av[0], errmsg);
|
|
goto restore_return;
|
|
}
|
|
/* Prepare to keep the child's stdout if requested */
|
|
if (pipe(p) == -1) {
|
|
error("%s: pipe: %s", tag, strerror(errno));
|
|
restore_return:
|
|
if (restore_privs != NULL)
|
|
restore_privs();
|
|
return 0;
|
|
}
|
|
if (restore_privs != NULL)
|
|
restore_privs();
|
|
|
|
switch ((pid = fork())) {
|
|
case -1: /* error */
|
|
error("%s: fork: %s", tag, strerror(errno));
|
|
close(p[0]);
|
|
close(p[1]);
|
|
return 0;
|
|
case 0: /* child */
|
|
/* Prepare a minimal environment for the child. */
|
|
if ((flags & SSH_SUBPROCESS_PRESERVE_ENV) == 0) {
|
|
nenv = 5;
|
|
env = xcalloc(sizeof(*env), nenv);
|
|
child_set_env(&env, &nenv, "PATH", _PATH_STDPATH);
|
|
child_set_env(&env, &nenv, "USER", pw->pw_name);
|
|
child_set_env(&env, &nenv, "LOGNAME", pw->pw_name);
|
|
child_set_env(&env, &nenv, "HOME", pw->pw_dir);
|
|
if ((cp = getenv("LANG")) != NULL)
|
|
child_set_env(&env, &nenv, "LANG", cp);
|
|
}
|
|
|
|
for (i = 1; i < NSIG; i++)
|
|
ssh_signal(i, SIG_DFL);
|
|
|
|
if ((devnull = open(_PATH_DEVNULL, O_RDWR)) == -1) {
|
|
error("%s: open %s: %s", tag, _PATH_DEVNULL,
|
|
strerror(errno));
|
|
_exit(1);
|
|
}
|
|
if (dup2(devnull, STDIN_FILENO) == -1) {
|
|
error("%s: dup2: %s", tag, strerror(errno));
|
|
_exit(1);
|
|
}
|
|
|
|
/* Set up stdout as requested; leave stderr in place for now. */
|
|
fd = -1;
|
|
if ((flags & SSH_SUBPROCESS_STDOUT_CAPTURE) != 0)
|
|
fd = p[1];
|
|
else if ((flags & SSH_SUBPROCESS_STDOUT_DISCARD) != 0)
|
|
fd = devnull;
|
|
if (fd != -1 && dup2(fd, STDOUT_FILENO) == -1) {
|
|
error("%s: dup2: %s", tag, strerror(errno));
|
|
_exit(1);
|
|
}
|
|
closefrom(STDERR_FILENO + 1);
|
|
|
|
if (geteuid() == 0 &&
|
|
initgroups(pw->pw_name, pw->pw_gid) == -1) {
|
|
error("%s: initgroups(%s, %u): %s", tag,
|
|
pw->pw_name, (u_int)pw->pw_gid, strerror(errno));
|
|
_exit(1);
|
|
}
|
|
if (setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) == -1) {
|
|
error("%s: setresgid %u: %s", tag, (u_int)pw->pw_gid,
|
|
strerror(errno));
|
|
_exit(1);
|
|
}
|
|
if (setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) == -1) {
|
|
error("%s: setresuid %u: %s", tag, (u_int)pw->pw_uid,
|
|
strerror(errno));
|
|
_exit(1);
|
|
}
|
|
/* stdin is pointed to /dev/null at this point */
|
|
if ((flags & SSH_SUBPROCESS_STDOUT_DISCARD) != 0 &&
|
|
dup2(STDIN_FILENO, STDERR_FILENO) == -1) {
|
|
error("%s: dup2: %s", tag, strerror(errno));
|
|
_exit(1);
|
|
}
|
|
if (env != NULL)
|
|
execve(av[0], av, env);
|
|
else
|
|
execv(av[0], av);
|
|
error("%s %s \"%s\": %s", tag, env == NULL ? "execv" : "execve",
|
|
command, strerror(errno));
|
|
_exit(127);
|
|
default: /* parent */
|
|
break;
|
|
}
|
|
|
|
close(p[1]);
|
|
if ((flags & SSH_SUBPROCESS_STDOUT_CAPTURE) == 0)
|
|
close(p[0]);
|
|
else if ((f = fdopen(p[0], "r")) == NULL) {
|
|
error("%s: fdopen: %s", tag, strerror(errno));
|
|
close(p[0]);
|
|
/* Don't leave zombie child */
|
|
kill(pid, SIGTERM);
|
|
while (waitpid(pid, NULL, 0) == -1 && errno == EINTR)
|
|
;
|
|
return 0;
|
|
}
|
|
/* Success */
|
|
debug3_f("%s pid %ld", tag, (long)pid);
|
|
if (child != NULL)
|
|
*child = f;
|
|
return pid;
|
|
}
|
|
|
|
const char *
|
|
lookup_env_in_list(const char *env, char * const *envs, size_t nenvs)
|
|
{
|
|
size_t i, envlen;
|
|
|
|
envlen = strlen(env);
|
|
for (i = 0; i < nenvs; i++) {
|
|
if (strncmp(envs[i], env, envlen) == 0 &&
|
|
envs[i][envlen] == '=') {
|
|
return envs[i] + envlen + 1;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
const char *
|
|
lookup_setenv_in_list(const char *env, char * const *envs, size_t nenvs)
|
|
{
|
|
char *name, *cp;
|
|
const char *ret;
|
|
|
|
name = xstrdup(env);
|
|
if ((cp = strchr(name, '=')) == NULL) {
|
|
free(name);
|
|
return NULL; /* not env=val */
|
|
}
|
|
*cp = '\0';
|
|
ret = lookup_env_in_list(name, envs, nenvs);
|
|
free(name);
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Helpers for managing poll(2)/ppoll(2) timeouts
|
|
* Will remember the earliest deadline and return it for use in poll/ppoll.
|
|
*/
|
|
|
|
/* Initialise a poll/ppoll timeout with an indefinite deadline */
|
|
void
|
|
ptimeout_init(struct timespec *pt)
|
|
{
|
|
/*
|
|
* Deliberately invalid for ppoll(2).
|
|
* Will be converted to NULL in ptimeout_get_tspec() later.
|
|
*/
|
|
pt->tv_sec = -1;
|
|
pt->tv_nsec = 0;
|
|
}
|
|
|
|
/* Specify a poll/ppoll deadline of at most 'sec' seconds */
|
|
void
|
|
ptimeout_deadline_sec(struct timespec *pt, long sec)
|
|
{
|
|
if (pt->tv_sec == -1 || pt->tv_sec >= sec) {
|
|
pt->tv_sec = sec;
|
|
pt->tv_nsec = 0;
|
|
}
|
|
}
|
|
|
|
/* Specify a poll/ppoll deadline of at most 'p' (timespec) */
|
|
static void
|
|
ptimeout_deadline_tsp(struct timespec *pt, struct timespec *p)
|
|
{
|
|
if (pt->tv_sec == -1 || timespeccmp(pt, p, >=))
|
|
*pt = *p;
|
|
}
|
|
|
|
/* Specify a poll/ppoll deadline of at most 'ms' milliseconds */
|
|
void
|
|
ptimeout_deadline_ms(struct timespec *pt, long ms)
|
|
{
|
|
struct timespec p;
|
|
|
|
p.tv_sec = ms / 1000;
|
|
p.tv_nsec = (ms % 1000) * 1000000;
|
|
ptimeout_deadline_tsp(pt, &p);
|
|
}
|
|
|
|
/* Specify a poll/ppoll deadline at wall clock monotime 'when' (timespec) */
|
|
void
|
|
ptimeout_deadline_monotime_tsp(struct timespec *pt, struct timespec *when)
|
|
{
|
|
struct timespec now, t;
|
|
|
|
monotime_ts(&now);
|
|
|
|
if (timespeccmp(&now, when, >=)) {
|
|
/* 'when' is now or in the past. Timeout ASAP */
|
|
pt->tv_sec = 0;
|
|
pt->tv_nsec = 0;
|
|
} else {
|
|
timespecsub(when, &now, &t);
|
|
ptimeout_deadline_tsp(pt, &t);
|
|
}
|
|
}
|
|
|
|
/* Specify a poll/ppoll deadline at wall clock monotime 'when' */
|
|
void
|
|
ptimeout_deadline_monotime(struct timespec *pt, time_t when)
|
|
{
|
|
struct timespec t;
|
|
|
|
t.tv_sec = when;
|
|
t.tv_nsec = 0;
|
|
ptimeout_deadline_monotime_tsp(pt, &t);
|
|
}
|
|
|
|
/* Get a poll(2) timeout value in milliseconds */
|
|
int
|
|
ptimeout_get_ms(struct timespec *pt)
|
|
{
|
|
if (pt->tv_sec == -1)
|
|
return -1;
|
|
if (pt->tv_sec >= (INT_MAX - (pt->tv_nsec / 1000000)) / 1000)
|
|
return INT_MAX;
|
|
return (pt->tv_sec * 1000) + (pt->tv_nsec / 1000000);
|
|
}
|
|
|
|
/* Get a ppoll(2) timeout value as a timespec pointer */
|
|
struct timespec *
|
|
ptimeout_get_tsp(struct timespec *pt)
|
|
{
|
|
return pt->tv_sec == -1 ? NULL : pt;
|
|
}
|
|
|
|
/* Returns non-zero if a timeout has been set (i.e. is not indefinite) */
|
|
int
|
|
ptimeout_isset(struct timespec *pt)
|
|
{
|
|
return pt->tv_sec != -1;
|
|
}
|
|
|
|
/*
|
|
* Returns zero if the library at 'path' contains symbol 's', nonzero
|
|
* otherwise.
|
|
*/
|
|
int
|
|
lib_contains_symbol(const char *path, const char *s)
|
|
{
|
|
#ifdef HAVE_NLIST_H
|
|
struct nlist nl[2];
|
|
int ret = -1, r;
|
|
|
|
memset(nl, 0, sizeof(nl));
|
|
nl[0].n_name = xstrdup(s);
|
|
nl[1].n_name = NULL;
|
|
if ((r = nlist(path, nl)) == -1) {
|
|
error_f("nlist failed for %s", path);
|
|
goto out;
|
|
}
|
|
if (r != 0 || nl[0].n_value == 0 || nl[0].n_type == 0) {
|
|
error_f("library %s does not contain symbol %s", path, s);
|
|
goto out;
|
|
}
|
|
/* success */
|
|
ret = 0;
|
|
out:
|
|
free(nl[0].n_name);
|
|
return ret;
|
|
#else /* HAVE_NLIST_H */
|
|
int fd, ret = -1;
|
|
struct stat st;
|
|
void *m = NULL;
|
|
size_t sz = 0;
|
|
|
|
memset(&st, 0, sizeof(st));
|
|
if ((fd = open(path, O_RDONLY)) < 0) {
|
|
error_f("open %s: %s", path, strerror(errno));
|
|
return -1;
|
|
}
|
|
if (fstat(fd, &st) != 0) {
|
|
error_f("fstat %s: %s", path, strerror(errno));
|
|
goto out;
|
|
}
|
|
if (!S_ISREG(st.st_mode)) {
|
|
error_f("%s is not a regular file", path);
|
|
goto out;
|
|
}
|
|
if (st.st_size < 0 ||
|
|
(size_t)st.st_size < strlen(s) ||
|
|
st.st_size >= INT_MAX/2) {
|
|
error_f("%s bad size %lld", path, (long long)st.st_size);
|
|
goto out;
|
|
}
|
|
sz = (size_t)st.st_size;
|
|
if ((m = mmap(NULL, sz, PROT_READ, MAP_PRIVATE, fd, 0)) == MAP_FAILED ||
|
|
m == NULL) {
|
|
error_f("mmap %s: %s", path, strerror(errno));
|
|
goto out;
|
|
}
|
|
if (memmem(m, sz, s, strlen(s)) == NULL) {
|
|
error_f("%s does not contain expected string %s", path, s);
|
|
goto out;
|
|
}
|
|
/* success */
|
|
ret = 0;
|
|
out:
|
|
if (m != NULL && m != MAP_FAILED)
|
|
munmap(m, sz);
|
|
close(fd);
|
|
return ret;
|
|
#endif /* HAVE_NLIST_H */
|
|
}
|
|
|
|
int
|
|
signal_is_crash(int sig)
|
|
{
|
|
switch (sig) {
|
|
case SIGSEGV:
|
|
case SIGBUS:
|
|
case SIGTRAP:
|
|
case SIGSYS:
|
|
case SIGFPE:
|
|
case SIGILL:
|
|
case SIGABRT:
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|