From 83047d4413d09a2e49f8604de9c50fae345424e8 Mon Sep 17 00:00:00 2001 From: Joachim Nilsson Date: Sun, 9 Jun 2019 18:06:47 +0200 Subject: [PATCH] Add support for a simple telnet expect set of APIs Signed-off-by: Joachim Nilsson --- README.md | 9 ++ src/Makefile.am | 4 +- src/lite.h | 6 + src/telnet.c | 291 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 308 insertions(+), 2 deletions(-) create mode 100644 src/telnet.c diff --git a/README.md b/README.md index c63f1e3..75e4055 100644 --- a/README.md +++ b/README.md @@ -289,6 +289,15 @@ Generic Functions Find matching string in an array of strings. Returns index in array on match, or `-1` on error or not found. +- `telnet_open(), telnet_close(), telnet_expect()` + + Poor mans telnet expect in C. Opens connection to a Telnet service; + FTP, Telnet, similar, and run an expect-like script. + +- `telnet_session()` + + Wrapper for the above three in one API. + - `tempfile()` Secure replacement for `tmpfile()`. Creates an invisible temporary diff --git a/src/Makefile.am b/src/Makefile.am index 1167432..8f3bde6 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -14,8 +14,8 @@ libite_la_SOURCES = chomp.c copyfile.c \ reallocarray.c rsync.c \ strlcpy.c strlcat.c strtonum.c \ strdupa.h strndupa.h strnlen.h \ - strmatch.c yorn.c \ - tempfile.c which.c \ + strmatch.c telnet.c tempfile.c \ + yorn.c which.c \ lite.h strlite.h \ queue.h tree.h diff --git a/src/lite.h b/src/lite.h index 14f744a..f4423d2 100644 --- a/src/lite.h +++ b/src/lite.h @@ -57,6 +57,7 @@ extern "C" #define LITE_FOPT_KEEP_MTIME 0x02 typedef struct lfile lfile_t; +typedef struct sdbuf sdbuf_t; char *chomp (char *str); char *fparseln (FILE *, size_t *, size_t *, const char[3], int); @@ -95,6 +96,11 @@ void *reallocarray(void *optr, size_t nmemb, size_t size); void progress (int percent, int max_width); void progress_simple(int percent); +sdbuf_t*telnet_open (int addr, short port); +int telnet_close (sdbuf_t *ctx); +int telnet_expect (sdbuf_t *ctx, char *script[], FILE *output); +int telnet_session (int addr, short port, char *script[], FILE *output); + int yorn (const char *fmt, ...); char *which (const char *cmd); diff --git a/src/telnet.c b/src/telnet.c new file mode 100644 index 0000000..353215d --- /dev/null +++ b/src/telnet.c @@ -0,0 +1,291 @@ +/* Poor mans telnet expect in C. + * + * Copyright (c) 2010, 2013, 2019 Joachim Nilsson + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include + +#include "lite.h" + +#define EXPECT_DEBUG 0 +#define EXPECT_POLL_TIMEOUT 3000 /* 3 sec timeout waiting for server. */ + +#if EXPECT_DEBUG +# define TELL(fmt, args...) fprintf (stderr, fmt "\n", ##args) +#else +# define TELL(fmt, args...) +#endif + +struct sdbuf { + int sd; + char *buf; +} sdbuf; + +/* Open telnet connection to @addr:@port and connect. */ +sdbuf_t *telnet_open(int addr, short port) +{ + struct sockaddr_in *sin; + struct sockaddr s; + sdbuf_t *ctx; + int saved_errno; + + ctx = (sdbuf_t *)malloc(sizeof(sdbuf_t)); + if (!ctx) { +// perror("Failed allocating expect ctxect"); + return NULL; + } + + ctx->buf = (char *)malloc(BUFSIZ); + if (!ctx->buf) { +// perror("Failed allocating expect buffer"); + free(ctx); + return NULL; + } + + ctx->sd = socket(PF_INET, SOCK_STREAM, 0); + if (-1 == ctx->sd) { + saved_errno = errno; +// perror ("Failed creating socket"); + free(ctx->buf); + free(ctx); + + errno = saved_errno; + return NULL; + } + + sin = (struct sockaddr_in *)&s; + sin->sin_family = AF_INET; + sin->sin_port = port; + sin->sin_addr.s_addr = addr; + if (-1 == connect(ctx->sd, &s, sizeof(s))) { + saved_errno = errno; +// perror ("Cannot connect to telnet daemon"); + telnet_close(ctx); + errno = saved_errno; + + return NULL; + } + + return ctx; +} + +int telnet_close(sdbuf_t *ctx) +{ + free(ctx->buf); + close(ctx->sd); + free(ctx); + + return 0; +} + +static int wait_substr(sdbuf_t *ctx, char *str) +{ + struct pollfd pfd = { ctx->sd, POLLIN, 0 }; + ssize_t len, tot = 0; + int err; + + while (1) { + err = poll(&pfd, 1, EXPECT_POLL_TIMEOUT); + if (err <= 0) { + if (0 == err) + errno = ETIMEDOUT; + + if (EINTR == errno) + continue; + +// perror("No prompt"); + return 1; + } + + len = read(ctx->sd, &ctx->buf[tot], BUFSIZ); + if (len <= 0) { + errno = ENOMSG; + return 1; + } + + tot += len; + ctx->buf[tot] = 0; + TELL("Got line: '%s' is it '%s'?", ctx->buf, str); + + if (strstr(ctx->buf, str)) + return 0; + } +} + +/** + * telnet_expect - Poor man's telnet expect + * @ctx: Telnet session context from telnet_open() + * @script: %NULL terminated list of expect and response strings + * @output: Optional output from session + * + * Issues @script sequence on telnet session specified in @ctx, with + * optional @output from session. + * + * The @script consists of strings of expect and response pairs. For + * example, expect "ogin: " followed by response "root\n", expect + * "assword: "with response "secret\n", concluded by %NULL. + * + * Returns: + * POSIX OK(0) or non-zero on error. + */ +int telnet_expect(sdbuf_t *ctx, char *script[], FILE *output) +{ + struct pollfd fd = { ctx->sd, POLLIN, 0 }; + ssize_t len; + int i, first = 1, cont = 1, result = 0; + + for (i = 0; script[i]; i++) { + /* when expecting the empty string, just wipe any available input */ + if (!strlen(script[i])) { + result = poll(&fd, 1, 0); + if (result > 0) + lseek(ctx->sd, 0, SEEK_END); + } else { + if (wait_substr(ctx, script[i])) { + result = ENOMSG; + return 1; + } + } + + i++; + + if (strlen(script[i])) { + TELL("Sending line: '%s'", script[i]); + if (-1 == write(ctx->sd, script[i], strlen(script[i]))) { + errno = ENOSPC; + return 1; + } + } + } + + while (cont) { + char *ptr = ctx->buf; + char *tmp; + + result = poll(&fd, 1, EXPECT_POLL_TIMEOUT); + if (result <= 0) { + if (0 == result) + errno = ETIMEDOUT; + + if (EINTR == errno) + continue; + +// perror ("No result"); + result = errno; + break; + } + + /* + * On VERY long outputs we need to send some love back to the + * server, otherwise it terminates the connection. --Jocke + */ + if (-1 == write(ctx->sd, "\n", 1)) + goto retry; + + len = read(ctx->sd, ctx->buf, BUFSIZ); + if (len == -1) { + retry: + if (EINTR == errno) + continue; + + result = errno; + break; + } + ctx->buf[len] = 0; + + TELL("Read line: '%s'", ctx->buf); + + /* Skip first line -- only an echo of the last command */ + if (first) { + first = 0; + ptr = strchr(ctx->buf, '\n'); + if (ptr) + ptr++; + else + ptr = ctx->buf; + } + + TELL("Comparing '%s' with '%s'", ptr, script[i - 2]); + + /* Skip last line -- only the returning prompt. */ + tmp = strstr(ptr, script[i - 2]); + if (tmp) { + TELL("OK, last line. Good bye!"); + result = 0; + cont = 0; + *tmp = 0; + + tmp = strrchr(ptr, '\n'); + if (tmp) + *++tmp = 0; + } + + if (output && *ptr) { + if (EOF == fputs(ptr, output)) { + /* Only happens when /tmp is full :( */ + result = ENOSPC; + /* We must complete read-out of descriptor */ + output = NULL; + } + } + } + + return result; +} + +/** + * telnet_session - Very simple expect-like implementation for telnet. + * @addr: Must be in network byte order, use htonl(). + * @port: IP port to connect to, use htons(). + * @script: Expect like script of paired 'expect', 'response' strings. + * @output: A &FILE pointer to a tmpfile() for output from the last command. + * + * This is a very simple expect-like implementation for querying and + * operating daemons remotely over telnet. + * + * The @script is a query-response type of list of strings leading up to + * a final command, which output is then written to the given @output file. + * + * Returns: + * POSIX OK(0), or non-zero with errno set on error. + */ +int telnet_session(int addr, short port, char *script[], FILE *output) +{ + sdbuf_t *sdbuf; + int result = 0; + + sdbuf = telnet_open(addr, port); + if (!sdbuf) + return errno; + + if (telnet_expect(sdbuf, script, output)) + result = errno; + + telnet_close(sdbuf); + + return errno = result; +} + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "linux" + * End: + */