From 2af6089436d27041cbaee2c230b5cd7350f50226 Mon Sep 17 00:00:00 2001 From: Gerhard Rieger Date: Thu, 26 Oct 2023 16:57:39 +0200 Subject: [PATCH] Socat option -r,-R path specifications allow use of variables --- CHANGES | 4 + doc/socat.yo | 10 ++- socat.c | 94 +++++++++-------------- sysutils.c | 205 +++++++++++++++++++++++++++++++++++++++++++++++++++ sysutils.h | 1 + test.sh | 98 ++++++++++++++++++++++++ utils.c | 1 - xio.h | 4 + xioparam.c | 26 ++++++- 9 files changed, 378 insertions(+), 65 deletions(-) diff --git a/CHANGES b/CHANGES index f9d4f05..a1c1a60 100644 --- a/CHANGES +++ b/CHANGES @@ -37,6 +37,10 @@ Features: Test: RCVTIMEO_DTLS Feature proposed by Vladimir Nikishkin. + The file names with -r and -R now may contain environment variable + references. + Test: VARS_IN_SNIFFPATH + Corrections: When a sub process (EXEC, SYSTEM) terminated with exit code other than 0, its last sent data might have been lost depending on timing of read/ diff --git a/doc/socat.yo b/doc/socat.yo index b7689b7..2fdca50 100644 --- a/doc/socat.yo +++ b/doc/socat.yo @@ -161,12 +161,16 @@ dit(bf(tt(-x))) Writes the transferred data not only to their target streams, but also to stderr. The output format is hexadecimal, prefixed with "> " or "< " indicating flow directions. Can be combined with code(-v). -dit(bf(tt(-r ))) +label(option_r)dit(bf(tt(-r ))) Dumps the raw (binary) data flowing from left to right address to the given - file. + file. The file name may contain references to environment variables and + code($$) (pid), code($PROGNAME) (see option link(option -lp)(option_lp)), + code($TIMESTAMP) (uses format %Y%m%dT%H%M%S), and code(MICROS) (microseconds + of daytime). These references have to be protected from shell expansion of + course. dit(bf(tt(-R ))) Dumps the raw (binary) data flowing from right to left address to the given - file. + file. See link(option -r)(option_r) for customization of file name. label(option_b)dit(bf(tt(-b))tt()) Sets the data transfer block [link(size_t)(TYPE_SIZE_T)]. At most bytes are transferred per step. Default is 8192 bytes. diff --git a/socat.c b/socat.c index 9f09305..fa39d3b 100644 --- a/socat.c +++ b/socat.c @@ -36,8 +36,6 @@ struct { char logopt; /* y..syslog; s..stderr; f..file; m..mixed */ bool lefttoright; /* first addr ro, second addr wo */ bool righttoleft; /* first addr wo, second addr ro */ - int sniffleft; /* -1 or an FD for teeing data arriving on xfd1 */ - int sniffright; /* -1 or an FD for teeing data arriving on xfd2 */ xiolock_t lock; /* a lock file */ unsigned long log_sigs; /* signals to be caught just for logging */ } socat_opts = { @@ -52,8 +50,6 @@ struct { 's', /* logopt */ false, /* lefttoright */ false, /* righttoleft */ - -1, /* sniffleft */ - -1, /* sniffright */ { NULL, 0 }, /* lock */ 1<= 0) Close(sniffleft); + sniffleft = xio_opensnifffile(name, &tv); + if (sniffleft < 0) { + Error2("option -r \"%s\": %s", name, strerror(errno)); + } + } + + if (xioinqopt('R', name, sizeof(name)) == 0) { + if (sniffright >= 0) Close(sniffright); + sniffright = xio_opensnifffile(name, &tv); + if (sniffright < 0) { + Error2("option -R \"%s\": %s", name, strerror(errno)); + } + } + } + #if WITH_FILAN if (socat_opts.debug) { int fdi, fdo; @@ -1398,23 +1372,23 @@ int xiotransfer(xiofile_t *inpipe, xiofile_t *outpipe, errno = EAGAIN; return -1; } - if (!righttoleft && socat_opts.sniffleft >= 0) { - if ((sniffed = Write(socat_opts.sniffleft, buff, bytes)) < bytes) { + if (!righttoleft && sniffleft >= 0) { + if ((sniffed = Write(sniffleft, buff, bytes)) < bytes) { if (sniffed < 0) Warn3("-r: write(%d, buff, "F_Zu"): %s", - socat_opts.sniffleft, bytes, strerror(errno)); + sniffleft, bytes, strerror(errno)); else if (sniffed < bytes) Warn3("-r: write(%d, buff, "F_Zu") -> "F_Zd, - socat_opts.sniffleft, bytes, sniffed); + sniffleft, bytes, sniffed); } - } else if (righttoleft && socat_opts.sniffright >= 0) { - if ((sniffed = Write(socat_opts.sniffright, buff, bytes)) < bytes) { + } else if (righttoleft && sniffright >= 0) { + if ((sniffed = Write(sniffright, buff, bytes)) < bytes) { if (sniffed < 0) Warn3("-R: write(%d, buff, "F_Zu"): %s", - socat_opts.sniffright, bytes, strerror(errno)); + sniffright, bytes, strerror(errno)); else if (sniffed < bytes) Warn3("-R: write(%d, buff, "F_Zu") -> "F_Zd, - socat_opts.sniffright, bytes, sniffed); + sniffright, bytes, sniffed); } } diff --git a/sysutils.c b/sysutils.c index 90b282f..6c9b1c6 100644 --- a/sysutils.c +++ b/sysutils.c @@ -941,3 +941,208 @@ double Strtod(const char *nptr, char **endptr, const char *txt) { } return res; } + + +/* This function gets a string with possible references to environment + variables and expands them. Variables must be prefixed with '$' and their + names consist only of A-Z, a-z, 0-9, and '_'. + The name may be enclosed with { }. + To pass a literal "$" us "\$". + There are special variables supported whose values do not comr from + environment: + $$ expands to the process's pid + $PROGNAME expands to the executables basename or the value of option -lp + $TIMESTAMP expands to the actual time in format %Y%m%dT%H%M%S + $MICROS expands to the actual microseconds (decimal) + The dst output is a copy of src but with the variables expanded. + Returns 0 on success; + returns -1 when the output buffer was too short (overflow); + returns 1 on syntax error. +*/ +int expandenv( + char *dst, /* prealloc'd output buff, will be \0 termd */ + const char *src, /* input string to generate expansion from */ + size_t n, /* length of dst */ + struct timeval *tv) /* in/out timestamp for sync */ +{ + char c; /* char currently being lex'd/parsed */ + bool esc = false; /* just got '\' */ + bool bra = false; /* within ${ } */ + char *nam = NULL; /* points to temp.allocated rw copy of src */ + const char *e; /* pointer to isolated var name in tmp[] */ + size_t s=0, d=0; /* counters in src, dst */ + size_t v; /* variable name begin */ + bool ofl = false; /* dst overflow, output truncated */ + char tmp[18]; /* buffer for timestamp, micros */ + + while (c = src[s++]) { + if (esc) { + if (c == '\0') { + if (d+2 > n) { ofl = true; break; } + dst[d++] = '\\'; + dst[d++] = c; + break; + } + if (c != '$') { + if (d+3 > n) { ofl = true; break; } + dst[d++] = '\\'; + } else { + if (d+2 > n) { ofl = true; break; } + } + dst[d++] = c; + esc = false; + continue; + } + if (c == '\0') { + dst[d++] = c; + break; + } + if (c == '\\') { + esc = true; + continue; + } + if (c != '$') { + if (d+2 > n) { ofl = true; break; } + dst[d++] = c; + continue; + } + + /* c == '$': Expecting env var to expand */ + c = src[s]; + if (c == '\0') { + if (d+2 > n) { ofl = true; break; } + ++s; + dst[d++] = '$'; + break; + } + if (c == '$') { + int wr; + /* Special case: pid */ + ++s; + wr = snprintf(&dst[d], n-d, F_pid, getpid()); + if (wr >= n-d || wr < 0) { ofl = true; break; } + d += wr; + continue; + } + if (c == '{') { + ++s; + bra = true; + } + if (!isalpha(c) && c != '_') { + /* Special case no var name, just keep '$' */ + if (d+3 > n) { ofl = true; break; } + ++s; + dst[d++] = '$'; + dst[d++] = c; + continue; + } + v = 0; /* seems we found valid variable name */ + if (nam == NULL) { + nam = strdup(src+s); + if (nam == NULL) { + errno = ENOMEM; + return -1; + } + } + while (c = src[s]) { + if (!isalnum(c) && c != '_') { + break; + } + nam[v++] = c; + ++s; + } + if (bra && c != '}') { + return 1; + } + + nam[v] = '\0'; + /* Var name is complete */ + /* Check hardcoded var names */ + if (strcmp(nam, "PROGNAME") == 0) { + e = diag_get_string('p'); + } else if (strcmp(nam, "TIMESTAMP") == 0) { + if (tv->tv_sec == 0) { + gettimeofday(tv, NULL); + } + struct tm tm; + localtime_r(&tv->tv_sec, &tm); + strftime(tmp, sizeof(tmp), "%Y%m%dT%H%M%S", &tm); + e = tmp; + } else if (strcmp(nam, "MICROS") == 0) { + if (tv->tv_sec == 0) { + gettimeofday(tv, NULL); + } + sprintf(tmp, F_tv_usec, tv->tv_usec); + e = tmp; + } else { + e = getenv(nam); + } + if (e == NULL) { + /* Var not found, skip it */ + continue; + } + /* Var found, copy it to output buffer */ + if (d+strlen(e)+1 > n) { ofl = true; break; } + strcpy(&dst[d], e); + d += strlen(&dst[d]); + continue; + } + + if (nam != NULL) { + free(nam); + } + + if (ofl) { + dst[d] = '\0'; + return -1; + } + dst[d] = '\0'; + if (src[s-1] != '\0') { + errno = EINVAL; + return -1; + } + return 0; +} + +int xio_opensnifffile( + const char *a, + struct timeval *tv) +{ + char path[PATH_MAX]; + int flags; + int rc; + int fd; + + rc = expandenv(path, a, sizeof(path), tv); + if (rc < 0) { + Error2("expandenv(source=\"%s\", n="F_Zu"): Out of memory", a, sizeof(path)); + errno = ENOMEM; + return -1; + } else if (rc > 0) { + Error1("expandenv(source=\"%s\"): Syntax error", a); + errno = EINVAL; + return -1; + } + flags = O_CREAT|O_WRONLY|O_APPEND| +#ifdef O_LARGEFILE + O_LARGEFILE| +#endif +#ifdef O_CLOEXEC + O_CLOEXEC| +#endif + O_NONBLOCK; + if ((fd = Open(path, flags, 0664)) < 0) { + if (errno == ENXIO) { + /* try to open pipe rdwr */ + if ((fd = Open(path, flags, 0664)) < 0) { + return -1; + } + } + } +#ifdef O_CLOEXEC + if (Fcntl_l(fd, F_SETFD, FD_CLOEXEC) < 0) { + Warn2("fcntl(%d, F_SETFD, FD_CLOEXEC): %s", fd, strerror(errno)); + } +#endif + return fd; +} diff --git a/sysutils.h b/sysutils.h index 98c150d..514d1d2 100644 --- a/sysutils.h +++ b/sysutils.h @@ -113,5 +113,6 @@ extern int xiosetenvushort(const char *varname, unsigned short value, extern unsigned long int Strtoul(const char *nptr, char **endptr, int base, const char *txt); extern long long int Strtoll(const char *nptr, char **endptr, int base, const char *txt); extern double Strtod(const char *nptr, char **endptr, const char *txt); +extern int xio_opensnifffile(const char *a, struct timeval *tv); #endif /* !defined(__sysutils_h_included) */ diff --git a/test.sh b/test.sh index b6c378f..edcfcfa 100755 --- a/test.sh +++ b/test.sh @@ -15224,6 +15224,7 @@ wait fi ;; # NUMCOND, feats esac PORT=$((PORT+1)) +N=$((N+1)) # Test the so-rcvtimeo address option with DTLS @@ -15297,6 +15298,103 @@ esac N=$((N+1)) +# Test the use of interesting variables in the sniffing file names +NAME=VARS_IN_SNIFFPATH +case "$TESTS" in +*%$N%*|*%functions%*|*%socket%*|*%$NAME%*) +TEST="$NAME: sniff file names with variables" +# Start a server process with option fork that sniffs traffic and stores it in +# two files for each child process, using PID, timestamp, microseconds, and +# client IP +# Connect two times. +# For now we say that the test succeeded when 4 log files have been generated. +if ! eval $NUMCOND; then :; +elif ! A=$(testfeats IP4 TCP LISTEN PIPE STDIO); then + $PRINTF "test $F_n $TEST... ${YELLOW}Feature $F not available${NORMAL}\n" $N + numCANT=$((numCANT+1)) + listCANT="$listCANT $N" +elif ! A=$(testaddrs - TCP4 TCP4-LISTEN PIPE STDIO); then + $PRINTF "test $F_n $TEST... ${YELLOW}Address $A not available in $SOCAT${NORMAL}\n" $N + numCANT=$((numCANT+1)) + listCANT="$listCANT $N" +elif ! o=$(testoptions so-reuseaddr fork) >/dev/null; then + $PRINTF "test $F_n $TEST... ${YELLOW}Option $o not available in $SOCAT${NORMAL}\n" $N + numCANT=$((numCANT+1)) + listCANT="$listCANT $N" +elif ! runsip4 >/dev/null; then + $PRINTF "test $F_n $TEST... ${YELLOW}IPv4 not available${NORMAL}\n" $N + numCANT=$((numCANT+1)) + listCANT="$listCANT $N" +else +tf="$td/test$N.stdout" +te="$td/test$N.stderr" +tdiff="$td/test$N.diff" +da="test$N $(date) $RANDOM" +newport tcp4 # or whatever proto, or drop this line +CMD0="$TRACE $SOCAT $opts -lp server0 -r \"$td/test$N.\\\$PROGNAME-\\\$TIMESTAMP.\\\$MICROS-\\\$SERVER0_PEERADDR-\\\$\\\$.in.log\" -R \"$td/test$N.\\\$PROGNAME-\\\$TIMESTAMP.\\\$MICROS-\\\$SERVER0_PEERADDR-\\\$\\\$.out.log\" TCP4-LISTEN:$PORT,so-reuseaddr,fork PIPE" +CMD1="$TRACE $SOCAT $opts - TCP4-CONNECT:$LOCALHOST:$PORT" +printf "test $F_n $TEST... " $N +eval "$CMD0" >/dev/null 2>"${te}0" & +pid0=$! +waittcp4port $PORT 1 +echo "$da" |$CMD1 >"${tf}1a" 2>"${te}1a" +rc1a=$? +echo "$da" |$CMD1 >"${tf}1b" 2>"${te}1b" +rc1b=$? +kill $(childpids $pid0) $pid0 2>/dev/null; wait +if [ $rc1a != 0 -o $rc1b != 0 ]; then + $PRINTF "$FAILED (client problem)\n" + echo "$CMD0 &" + cat "${te}0" >&2 + echo "$CMD1" + cat "${te}1a" >&2 + echo "$CMD1" + cat "${te}1b" >&2 + numFAIL=$((numFAIL+1)) + listFAIL="$listFAIL $N" + namesFAIL="$namesFAIL $NAME" +elif test $(ls -l $td/test$N.*.log |wc -l) -eq 4 && + test $(ls $td/test$N.*.log |head -n 1 |wc -c) -ge 56; then + # Are the names correct? + # Convert timestamps to epoch and compare + # Are the contents correct? + $PRINTF "$OK\n" + if [ "$VERBOSE" ]; then echo "$CMD0 &"; fi + if [ "$DEBUG" ]; then cat "${te}0" >&2; fi + if [ "$VERBOSE" ]; then echo "$CMD1"; fi + if [ "$DEBUG" ]; then cat "${te}1a" >&2; fi + if [ "$VERBOSE" ]; then echo "$CMD1"; fi + if [ "$DEBUG" ]; then cat "${te}1b" >&2; fi + numOK=$((numOK+1)) +elif test -f $td/test$N.\$PROGNAME-\$TIMESTAMP.\$MICROS-\$SERVER0_PEERADDR-\$\$.in.log; then + $PRINTF "$FAILED (vars not resolved)\n" + echo "$CMD0 &" + cat "${te}0" >&2 + echo "$CMD1" + cat "${te}1a" >&2 + echo "$CMD1" + cat "${te}1b" >&2 + numFAIL=$((numFAIL+1)) + listFAIL="$listFAIL $N" + namesFAIL="$namesFAIL $NAME" +else + $PRINTF "$FAILED (unknown)\n" + echo "$CMD0 &" + cat "${te}0" >&2 + echo "$CMD1" + cat "${te}1a" >&2 + echo "$CMD1" + cat "${te}1b" >&2 + numFAIL=$((numFAIL+1)) + listFAIL="$listFAIL $N" + namesFAIL="$namesFAIL $NAME" +fi +fi # NUMCOND + ;; +esac +N=$((N+1)) + + # end of common tests ################################################################################## diff --git a/utils.c b/utils.c index 5760831..7243ca9 100644 --- a/utils.c +++ b/utils.c @@ -89,7 +89,6 @@ int setenv(const char *name, const char *value, int overwrite) { #endif /* !HAVE_SETENV */ - /* sanitizes an "untrusted" character. output buffer must provide at least 4 characters space. Does not append \0. returns length of output (currently: max 4) */ diff --git a/xio.h b/xio.h index 4e5bc49..33c2eb4 100644 --- a/xio.h +++ b/xio.h @@ -112,6 +112,8 @@ typedef struct { char preferred_ip; /* preferred prot.fam. for name resolution ('0' for unspecified, '4', or '6') */ bool experimental; /* enable some features */ + const char *sniffleft_name; /* file name with -r */ + const char *sniffright_name; /* file name with -R */ } xioparms_t; /* pack the description of a lock file */ @@ -440,6 +442,8 @@ extern int xioopenhelp(FILE *of, int level); /* must be outside function for use by childdied handler */ extern xiofile_t *sock1, *sock2; +extern int sniffleft, sniffright; + #define NUMUNKNOWN 4 extern pid_t diedunknown[NUMUNKNOWN]; /* child died before it is registered */ #define diedunknown1 (diedunknown[0]) diff --git a/xioparam.c b/xioparam.c index 4c13c0d..5a4d3da 100644 --- a/xioparam.c +++ b/xioparam.c @@ -21,7 +21,9 @@ xioparms_t xioparms = { NULL, /* syslogfac */ '4', /* default_ip */ '4', /* preferred_ip */ - false /* experimental */ + false, /* experimental */ + NULL, /* sniffleft_name */ + NULL /* sniffright_name */ } ; @@ -43,6 +45,8 @@ int xiosetopt(char what, const char *arg) { break; case 'l': xioparms.logopt = *arg; break; case 'y': xioparms.syslogfac = arg; break; + case 'r': xioparms.sniffleft_name = arg; break; + case 'R': xioparms.sniffright_name = arg; break; default: Error2("xiosetopt('%c', \"%s\"): unknown option", what, arg?arg:"NULL"); @@ -60,6 +64,26 @@ int xioinqopt(char what, char *arg, size_t n) { return 0; case 'o': return xioparms.ip4portsep; case 'l': return xioparms.logopt; + case 'r': + if (xioparms.sniffleft_name == NULL) { + return 1; + } + if (n < strlen(xioparms.sniffleft_name)+1) { + return -1; + } + arg[0] = '\0'; + strncat(arg, xioparms.sniffleft_name, n-1); + return 0; + case 'R': + if (xioparms.sniffright_name == NULL) { + return 1; + } + if (n < strlen(xioparms.sniffright_name)+1) { + return -1; + } + arg[0] = '\0'; + strncat(arg, xioparms.sniffright_name, n-1); + return 0; default: Error3("xioinqopt('%c', \"%s\", "F_Zu"): unknown option", what, arg, n);