/* ** Copyright (C) 2001-2025 Zabbix SIA ** ** This program is free software: you can redistribute it and/or modify it under the terms of ** the GNU Affero General Public License as published by the Free Software Foundation, version 3. ** ** This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; ** without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. ** See the GNU Affero General Public License for more details. ** ** You should have received a copy of the GNU Affero General Public License along with this program. ** If not, see <https://www.gnu.org/licenses/>. **/ #include "zbxcomms.h" #include "comms.h" #include "zbxlog.h" #include "zbxstr.h" #define CMD_IAC 255 #define CMD_WILL 251 #define CMD_WONT 252 #define CMD_DO 253 #define CMD_DONT 254 #define OPT_SGA 3 static char prompt_char = '\0'; static int telnet_waitsocket(ZBX_SOCKET socket_fd, short mode) { int rc; zbx_pollfd_t pd; zabbix_log(LOG_LEVEL_DEBUG, "In %s()", __func__); pd.fd = socket_fd; pd.events = mode; if (0 > (rc = zbx_socket_poll(&pd, 1, 100))) { zabbix_log(LOG_LEVEL_DEBUG, "%s() poll() error rc:%d errno:%d error:[%s]", __func__, rc, zbx_socket_last_error(), zbx_strerror_from_system(zbx_socket_last_error())); } else if (0 < rc && POLLIN != (pd.revents & (POLLIN | POLLERR | POLLHUP | POLLNVAL))) { char *errmsg; errmsg = socket_poll_error(pd.revents); zabbix_log(LOG_LEVEL_DEBUG, "%s() %s", __func__, errmsg); zbx_free(errmsg); rc = ZBX_PROTO_ERROR; } zabbix_log(LOG_LEVEL_DEBUG, "End of %s():%d", __func__, rc); return rc; } static ssize_t telnet_socket_read(zbx_socket_t *s, void *buf, size_t count) { ssize_t rc; int error; zabbix_log(LOG_LEVEL_DEBUG, "In %s()", __func__); while (ZBX_PROTO_ERROR == (rc = ZBX_TCP_READ(s->socket, buf, count))) { error = zbx_socket_last_error(); /* zabbix_log() resets the error code */ zabbix_log(LOG_LEVEL_DEBUG, "%s() rc:%ld errno:%d error:[%s]", __func__, (long int)rc, error, zbx_strerror_from_system(error)); #ifdef _WINDOWS if (WSAEWOULDBLOCK == error) #else if (EAGAIN == error) #endif { if (SUCCEED != zbx_socket_check_deadline(s)) { zabbix_log(LOG_LEVEL_DEBUG, "%s() timeout error", __func__); goto ret; } /* wait and if there is still an error or no input available */ /* we assume the other side has nothing more to say */ if (1 > (rc = telnet_waitsocket(s->socket, POLLIN))) goto ret; continue; } break; } /* when ZBX_TCP_READ returns 0, it means EOF - let's consider it a permanent error */ /* note that if telnet_waitsocket() is zero, it is not a permanent condition */ if (0 == rc) rc = ZBX_PROTO_ERROR; ret: zabbix_log(LOG_LEVEL_DEBUG, "End of %s():%ld", __func__, (long int)rc); return rc; } static ssize_t telnet_socket_write(zbx_socket_t *s, const void *buf, size_t count) { ssize_t rc; int error; zabbix_log(LOG_LEVEL_DEBUG, "In %s()", __func__); while (ZBX_PROTO_ERROR == (rc = ZBX_TCP_WRITE(s->socket, buf, count))) { error = zbx_socket_last_error(); /* zabbix_log() resets the error code */ zabbix_log(LOG_LEVEL_DEBUG, "%s() rc:%ld errno:%d error:[%s]", __func__, (long int)rc, error, zbx_strerror_from_system(error)); #ifdef _WINDOWS if (WSAEWOULDBLOCK == error) #else if (EAGAIN == error) #endif { if (SUCCEED != zbx_socket_check_deadline(s)) { zabbix_log(LOG_LEVEL_DEBUG, "%s() timeout error", __func__); break; } (void)telnet_waitsocket(s->socket, POLLOUT); continue; } break; } zabbix_log(LOG_LEVEL_DEBUG, "End of %s():%ld", __func__, (long int)rc); return rc; } #undef WAIT_READ #undef WAIT_WRITE static ssize_t telnet_read(zbx_socket_t *s, char *buf, size_t *buf_left, size_t *buf_offset) { unsigned char c, c1, c2, c3; ssize_t rc = ZBX_PROTO_ERROR; zabbix_log(LOG_LEVEL_DEBUG, "In %s()", __func__); for (;;) { if (1 > (rc = telnet_socket_read(s, &c1, 1))) break; zabbix_log(LOG_LEVEL_DEBUG, "%s() c1:[%x=%c]", __func__, c1, isprint(c1) ? c1 : ' '); switch (c1) { case CMD_IAC: while (0 == (rc = telnet_socket_read(s, &c2, 1))) ; if (ZBX_PROTO_ERROR == rc) goto end; zabbix_log(LOG_LEVEL_DEBUG, "%s() c2:%x", __func__, c2); switch (c2) { case CMD_IAC: /* only IAC needs to be doubled to be sent as data */ if (0 < *buf_left) { buf[(*buf_offset)++] = (char)c2; (*buf_left)--; } break; case CMD_WILL: case CMD_WONT: case CMD_DO: case CMD_DONT: while (0 == (rc = telnet_socket_read(s, &c3, 1))) ; if (ZBX_PROTO_ERROR == rc) goto end; zabbix_log(LOG_LEVEL_DEBUG, "%s() c3:%x", __func__, c3); /* reply to all options with "WONT" or "DONT", */ /* unless it is Suppress Go Ahead (SGA) */ c = CMD_IAC; telnet_socket_write(s, &c, 1); if (CMD_WONT == c2) c = CMD_DONT; /* the only valid response */ else if (CMD_DONT == c2) c = CMD_WONT; /* the only valid response */ else if (OPT_SGA == c3) c = (c2 == CMD_DO ? CMD_WILL : CMD_DO); else c = (c2 == CMD_DO ? CMD_WONT : CMD_DONT); telnet_socket_write(s, &c, 1); telnet_socket_write(s, &c3, 1); break; default: break; } break; default: if (0 < *buf_left) { buf[(*buf_offset)++] = (char)c1; (*buf_left)--; } break; } } end: zabbix_log(LOG_LEVEL_DEBUG, "End of %s():%d", __func__, (int)rc); return rc; } #undef CMD_IAC #undef CMD_WILL #undef CMD_WONT #undef CMD_DO #undef CMD_DONT #undef OPT_SGA /****************************************************************************** * * * Comments: converts CR+LF to Unix LF and clears CR+NUL * * * ******************************************************************************/ static void convert_telnet_to_unix_eol(char *buf, size_t *offset) { size_t i, sz = *offset, new_offset; new_offset = 0; for (i = 0; i < sz; i++) { if (i + 1 < sz && '\r' == buf[i] && '\n' == buf[i + 1]) /* CR+LF (Windows) */ { buf[new_offset++] = '\n'; i++; } else if (i + 1 < sz && '\r' == buf[i] && '\0' == buf[i + 1]) /* CR+NUL */ { i++; } else if (i + 1 < sz && '\n' == buf[i] && '\r' == buf[i + 1]) /* LF+CR */ { buf[new_offset++] = '\n'; i++; } else if ('\r' == buf[i]) /* CR */ { buf[new_offset++] = '\n'; } else buf[new_offset++] = buf[i]; } *offset = new_offset; } static void convert_unix_to_telnet_eol(const char *buf, size_t offset, char *out_buf, size_t *out_offset) { size_t i; *out_offset = 0; for (i = 0; i < offset; i++) { if ('\n' != buf[i]) { out_buf[(*out_offset)++] = buf[i]; } else { out_buf[(*out_offset)++] = '\r'; out_buf[(*out_offset)++] = '\n'; } } } static char telnet_lastchar(const char *buf, size_t offset) { while (0 < offset) { offset--; if (' ' != buf[offset]) return buf[offset]; } return '\0'; } static int telnet_rm_echo(char *buf, size_t *offset, const char *echo, size_t len) { if (0 == memcmp(buf, echo, len)) { *offset -= len; memmove(&buf[0], &buf[len], *offset * sizeof(char)); return SUCCEED; } return FAIL; } static void telnet_rm_prompt(const char *buf, size_t *offset) { unsigned char state = 0; /* 0 - init, 1 - prompt */ while (0 < *offset) { (*offset)--; if (0 == state && buf[*offset] == prompt_char) state = 1; if (1 == state && buf[*offset] == '\n') break; } } int zbx_telnet_test_login(zbx_socket_t *s) { char buf[MAX_BUFFER_LEN]; size_t sz, offset; int rc, ret = FAIL; zabbix_log(LOG_LEVEL_DEBUG, "In %s()", __func__); sz = sizeof(buf); offset = 0; while (ZBX_PROTO_ERROR != (rc = telnet_read(s, buf, &sz, &offset))) { if (':' == telnet_lastchar(buf, offset)) break; } convert_telnet_to_unix_eol(buf, &offset); zabbix_log(LOG_LEVEL_DEBUG, "%s() login prompt:'%.*s'", __func__, (int)offset, buf); if (ZBX_PROTO_ERROR != rc) ret = SUCCEED; zabbix_log(LOG_LEVEL_DEBUG, "End of %s():%s", __func__, zbx_result_string(ret)); return ret; } int zbx_telnet_login(zbx_socket_t *s, const char *username, const char *password, AGENT_RESULT *result) { char buf[MAX_BUFFER_LEN], c; size_t sz, offset; int rc, ret = FAIL; zabbix_log(LOG_LEVEL_DEBUG, "In %s()", __func__); sz = sizeof(buf); offset = 0; while (ZBX_PROTO_ERROR != (rc = telnet_read(s, buf, &sz, &offset))) { if (':' == telnet_lastchar(buf, offset)) break; } convert_telnet_to_unix_eol(buf, &offset); zabbix_log(LOG_LEVEL_DEBUG, "%s() login prompt:'%.*s'", __func__, (int)offset, buf); if (ZBX_PROTO_ERROR == rc) { SET_MSG_RESULT(result, zbx_strdup(NULL, "No login prompt.")); goto fail; } telnet_socket_write(s, username, strlen(username)); telnet_socket_write(s, "\r\n", 2); sz = sizeof(buf); offset = 0; while (ZBX_PROTO_ERROR != (rc = telnet_read(s, buf, &sz, &offset))) { if (':' == telnet_lastchar(buf, offset)) break; } convert_telnet_to_unix_eol(buf, &offset); zabbix_log(LOG_LEVEL_DEBUG, "%s() password prompt:'%.*s'", __func__, (int)offset, buf); if (ZBX_PROTO_ERROR == rc) { SET_MSG_RESULT(result, zbx_strdup(NULL, "No password prompt.")); goto fail; } telnet_socket_write(s, password, strlen(password)); telnet_socket_write(s, "\r\n", 2); sz = sizeof(buf); offset = 0; while (ZBX_PROTO_ERROR != (rc = telnet_read(s, buf, &sz, &offset))) { if ('$' == (c = telnet_lastchar(buf, offset)) || '#' == c || '>' == c || '%' == c) { prompt_char = c; break; } } convert_telnet_to_unix_eol(buf, &offset); zabbix_log(LOG_LEVEL_DEBUG, "%s() prompt:'%.*s'", __func__, (int)offset, buf); if (ZBX_PROTO_ERROR == rc) { SET_MSG_RESULT(result, zbx_strdup(NULL, "Login failed.")); goto fail; } ret = SUCCEED; fail: zabbix_log(LOG_LEVEL_DEBUG, "End of %s():%s", __func__, zbx_result_string(ret)); return ret; } int zbx_telnet_execute(zbx_socket_t *s, const char *command, AGENT_RESULT *result, const char *encoding) { char buf[MAX_BUFFER_LEN]; char *utf8_result, *err_msg = NULL, *command_lf = NULL, *command_crlf = NULL; size_t sz, offset; int rc, ret = FAIL; size_t i, offset_lf, offset_crlf; zabbix_log(LOG_LEVEL_DEBUG, "In %s()", __func__); /* `command' with multiple lines may contain CR+LF from the browser; */ /* it should be converted to plain LF to remove echo later on properly */ offset_lf = strlen(command); command_lf = (char *)zbx_malloc(command_lf, offset_lf + 1); zbx_strlcpy(command_lf, command, offset_lf + 1); convert_telnet_to_unix_eol(command_lf, &offset_lf); /* telnet protocol requires that end-of-line is transferred as CR+LF */ command_crlf = (char *)zbx_malloc(command_crlf, offset_lf * 2 + 1); convert_unix_to_telnet_eol(command_lf, offset_lf, command_crlf, &offset_crlf); telnet_socket_write(s, command_crlf, offset_crlf); telnet_socket_write(s, "\r\n", 2); sz = sizeof(buf); offset = 0; while (ZBX_PROTO_ERROR != (rc = telnet_read(s, buf, &sz, &offset))) { if (prompt_char == telnet_lastchar(buf, offset)) break; } convert_telnet_to_unix_eol(buf, &offset); zabbix_log(LOG_LEVEL_DEBUG, "%s() command output:'%.*s'", __func__, (int)offset, buf); if (ZBX_PROTO_ERROR == rc) { const char *err_msg_loc; if (SUCCEED == zbx_socket_check_deadline(s)) err_msg_loc = zbx_strerror_from_system(zbx_socket_last_error()); else err_msg_loc = "timeout occurred"; SET_MSG_RESULT(result, zbx_dsprintf(NULL, "Cannot find prompt after command execution: %s", err_msg_loc)); goto fail; } telnet_rm_echo(buf, &offset, command_lf, offset_lf); /* multi-line commands may have returned additional prompts; */ /* this is not a perfect solution, because in case of multiple */ /* multi-line shell statements these prompts might appear in */ /* the middle of the output, but we still try to be helpful by */ /* removing additional prompts at least from the beginning */ for (i = 0; i < offset_lf; i++) { if ('\n' == command_lf[i]) { if (SUCCEED != telnet_rm_echo(buf, &offset, "$ ", 2) && SUCCEED != telnet_rm_echo(buf, &offset, "# ", 2) && SUCCEED != telnet_rm_echo(buf, &offset, "> ", 2) && SUCCEED != telnet_rm_echo(buf, &offset, "% ", 2)) { break; } } } telnet_rm_echo(buf, &offset, "\n", 1); telnet_rm_prompt(buf, &offset); zabbix_log(LOG_LEVEL_DEBUG, "%s() stripped command output:'%.*s'", __func__, (int)offset, buf); if (MAX_BUFFER_LEN == offset) offset--; buf[offset] = '\0'; if (NULL == (utf8_result = zbx_convert_to_utf8(buf, offset, encoding, &err_msg))) { SET_MSG_RESULT(result, zbx_dsprintf(NULL, "Cannot convert result to utf8: %s.", err_msg)); zbx_free(err_msg); goto fail; } else { SET_TEXT_RESULT(result, utf8_result); ret = SUCCEED; } fail: zbx_free(command_lf); zbx_free(command_crlf); zabbix_log(LOG_LEVEL_DEBUG, "End of %s():%s", __func__, zbx_result_string(ret)); return ret; }