/* * tcp.c -- telnet protocol communication module for powwow * * Copyright (C) 1998,2002 by Massimiliano Ghilardi * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef TELOPT_NAWS # define TELOPT_NAWS 31 #endif #include #ifndef NEXT # include #endif #ifdef TERM # include "client.h" #endif #include "defines.h" #include "main.h" #include "utils.h" #include "tcp.h" #include "tty.h" #include "edit.h" #include "beam.h" #include "log.h" #ifdef TELOPTS # define TELOPTSTR(n) ((n) > NTELOPTS ? "unknown" : telopts[n]) #endif int tcp_fd = -1; /* current socket file descriptor * -1 means no socket */ int tcp_main_fd = -1; /* socket file descriptor of main connect. * -1 means no socket */ int tcp_max_fd = 0; /* highest used fd */ int tcp_count = 0; /* number of open connections */ int tcp_attachcount = 0; /* number of spawned commands */ int conn_max_index; /* 1 + highest used conn_list[] index */ connsess conn_list[MAX_CONNECTS]; /* connection list */ byte conn_table[MAX_FDSCAN]; /* fd -> index translation table */ fd_set fdset; /* set of descriptors to select() on */ /* * process suboptions. * so far, only terminal type is processed but future extensions are * window size, X display location, etc. */ static void dosubopt(byte *str) { char buf[256], *term; int len, err; if (str[0] == TELOPT_TTYPE) { if (str[1] == 1) { /* 1 == SEND */ #ifdef TELOPTS tty_printf("[got SB TERMINAL TYPE SEND]\n"); #endif if (!(term = getenv("TERM"))) term = "unknown"; sprintf(buf, "%c%c%c%c%.*s%c%c", IAC, SB, TELOPT_TTYPE, 0, 256-7, term, IAC, SE); /* 0 == IS */ len = strlen(term) + 6; while ((err = write(tcp_fd, buf, len)) < 0 && errno == EINTR) ; if (err != len) { errmsg("write subopt to socket"); return; } #ifdef TELOPTS tty_printf("[sent SB TERMINAL TYPE IS %s]\n", term); #endif } } } /* * send an option negotiation * 'what' is one of WILL, WONT, DO, DONT */ static void sendopt(byte what, byte opt) { static byte buf[3] = { IAC, 0, 0 }; int i; buf[1] = what; buf[2] = opt; while ((i = write(tcp_fd, buf, 3)) < 0 && errno == EINTR) ; if (i != 3) { errmsg("write option to socket"); return; } #ifdef TELOPTS tty_printf("[sent %s %s]\n", (what == WILL) ? "WILL" : (what == WONT) ? "WONT" : (what == DO) ? "DO" : (what == DONT) ? "DONT" : "error", TELOPTSTR(opt)); #endif } /* * connect to remote host * Warning: some voodoo code here */ int tcp_connect(const char *addr, int port) { struct addrinfo *host_info; struct addrinfo addrinfo; struct sockaddr_in6 address; int must_free = 0; int err, newtcp_fd; status(1); memset(&address, 0, sizeof address); memset(&addrinfo, 0, sizeof addrinfo); /* * inet_addr has a strange design: It is documented to return -1 for * malformed requests, but it is declared to return unsigned long! * Anyway, this works. */ #ifndef TERM switch (inet_pton(AF_INET6, addr, &address.sin6_addr)) { case -1: perror("inet_pton()"); return -1; case 1: address.sin6_family = AF_INET6; addrinfo.ai_family = AF_INET6; addrinfo.ai_socktype = SOCK_STREAM; addrinfo.ai_addr = (struct sockaddr *)&address; addrinfo.ai_addrlen = sizeof address; host_info = &addrinfo; break; case 0: if (opt_info) tty_printf("#looking up %s... ", addr); tty_flush(); addrinfo.ai_family = AF_INET6; addrinfo.ai_socktype = SOCK_STREAM; addrinfo.ai_flags = AI_V4MAPPED; int gai = getaddrinfo(addr, NULL, &addrinfo, &host_info); if (gai != 0) { tty_printf("failed to look up host: %s\n", gai_strerror(gai)); return -1; } must_free = 1; if (opt_info) tty_puts("found.\n"); break; default: abort(); } for (; host_info; host_info = host_info->ai_next) { newtcp_fd = socket(host_info->ai_family, host_info->ai_socktype, 0); if (newtcp_fd == -1) continue; if (newtcp_fd >= MAX_FDSCAN) { tty_printf("#connect: #error: too many open connections\n"); close(newtcp_fd); return -1; } tty_printf("#trying %s... ", addr); tty_flush(); ((struct sockaddr_in6 *)host_info->ai_addr)->sin6_port = htons(port); err = connect(newtcp_fd, host_info->ai_addr, host_info->ai_addrlen); if (err == -1) { /* CTRL-C pressed, or other errors */ errmsg("connect to host"); close(newtcp_fd); return -1; } break; } if (host_info == NULL) { errmsg("failed to connect"); return -1; } #else /* term */ if ((newtcp_fd = connect_server(0)) < 0) { tty_puts("\n#powwow: unable to connect to term server\n"); return -1; } else { if (newtcp_fd >= MAX_FDSCAN) { tty_printf("#connect: #error: too many open connections\n"); close(newtcp_fd); return -1; } send_command(newtcp_fd, C_PORT, 0, "%s:%d", addr, port); tty_puts("Connected to term server...\n"); #ifdef TERM_COMPRESS send_command(newtcp_fd, C_COMPRESS, 1, "y"); #endif send_command(newtcp_fd, C_DUMB, 1, 0); } #endif /* term */ tty_puts("connected!\n"); { /* * Now set some options on newtcp_fd : * First, no-nagle */ int opt = 1; # ifndef SOL_TCP # define SOL_TCP IPPROTO_TCP # endif if (setsockopt(newtcp_fd, SOL_TCP, TCP_NODELAY, &opt, sizeof(opt))) errmsg("setsockopt(TCP_NODELAY) failed"); /* TCP keep-alive */ if (setsockopt(newtcp_fd, SOL_SOCKET, SO_KEEPALIVE, &opt, sizeof(opt))) errmsg("setsockopt(SO_KEEPALIVE) failed"); /* * Then, close-on-exec: * we don't want children to inherit the socket! */ fcntl(newtcp_fd, F_SETFD, FD_CLOEXEC); } return newtcp_fd; } /* * we don't expect IAC commands here, except IAC IAC (a protected ASCII 255) * which we replace with a single IAC (a plain ASCII 255) */ int tcp_unIAC(char *buffer, int len) { char *s, *start, warnIACs = 1; if (!memchr(buffer, IAC, len)) return len; for (s = start = buffer; len > 0; buffer++, len--) { if (buffer[0] == (char)(byte)IAC) { if (len > 1 && buffer[1] == (char)(byte)IAC) buffer++, len--; else if (warnIACs) { PRINTF("#warning: received IAC inside MPI message, treating as IAC IAC.\n"); warnIACs = 0; } } *s++ = *buffer; } return s - start; } /* * the reverse step: protect ASCII 255 as IAC IAC * the dest buffer is assumed to be big enough to hold the whole data */ static int tcp_addIAC(char *dest, char *txt, int len) { char *s = dest; while (len-- > 0) { if ((*s++ = *txt++) == (char)(byte)IAC) *s++ = (char)(byte)IAC; } return s - dest; } /* * read a maximum of size chars from remote host * using the telnet protocol. return chars read. */ int tcp_read(int fd, char *buffer, int maxsize) { char state = CONN_LIST(fd).state; char old_state = CONN_LIST(fd).old_state; int i; static byte subopt[MAX_SUBOPT]; static int subchars; byte *p, *s, *linestart; char *ibuffer = buffer; if (state == GOT_R) { /* make room for the leading \r */ ++ibuffer; --maxsize; } while ((i = read(fd, ibuffer, maxsize)) < 0 && errno == EINTR) ; if (i == 0) { CONN_LIST(fd).state = NORMAL; tcp_close(NULL); return 0; } if (i < 0) { errmsg("read from socket"); return 0; } /* * scan through the buffer, * interpret telnet protocol escapes and MUME MPI messages */ p = (byte *)buffer; for (s = linestart = (byte *)ibuffer; i; s++, i--) { switch (state) { case NORMAL: case ALTNORMAL: case GOT_R: case GOT_N: /* * Some servers like to send NULs and other funny chars. * Clean up as much as possible. */ switch (*s) { case IAC: old_state = state; /* remember state to return to after IAC processing */ state = GOTIAC; break; case '\r': /* start counting \r, unless just got \n */ if (state == NORMAL || state == ALTNORMAL) { /* * ALTNORMAL is safe here: \r cannot be in MPI header, * and any previous MPI header has already been rejected */ state = GOT_R; } else if (state == GOT_N) /* after \n\r, we forbid MPI messages */ state = ALTNORMAL; break; case '\n': state = GOT_N; *p++ = *s; linestart = p; break; case '\0': /* skip NULs */ break; default: /* first, flush any missing \r */ if (state == GOT_R) *p++ = '\r'; *p++ = *s; /* check for MUME MPI messages: */ if (p - linestart == MPILEN && !memcmp(linestart, MPI, MPILEN)) { if (!(CONN_LIST(fd).flags & IDEDITOR)) { PRINTF("#warning: MPI message received without #request editor!\n"); } else if (state == ALTNORMAL) { /* no MPI messages after \n\r */ PRINTF("#warning: MPI attack?\n"); } else { subchars = process_message((char*)s+1, i-1); /* no +MPILEN here, as it was already processed. */ s += subchars; i-= subchars; p = linestart; } } if (state != ALTNORMAL) state = NORMAL; break; } break; case GOTIAC: switch (*s) { case WILL: state = GOTWILL; break; case WONT: state = GOTWONT; break; case DO: state = GOTDO; break; case DONT: state = GOTDONT; break; case SB: /* BUG (multiple connections): */ state = GOTSB; /* there is only one subopt buffer */ subchars = 0; break; case IAC: *p++ = IAC; state = old_state; break; case GA: /* I should handle GA as end-of-prompt marker one day */ /* one day has come ;) - Max */ prompt_set_iac((char*)p); state = old_state; break; default: /* ignore the rest of the telnet commands */ #ifdef TELOPTS tty_printf("[skipped IAC <%d>]\n", *s); #endif state = old_state; break; } break; case GOTWILL: #ifdef TELOPTS tty_printf("[got WILL %s]\n", TELOPTSTR(*s)); #endif switch(*s) { case TELOPT_ECHO: /* host echoes, turn off echo here * but only for main connection, since we do not want * subsidiary connection password entries to block anything * in the main connection */ if (fd == tcp_main_fd) linemode |= LM_NOECHO; sendopt(DO, *s); break; case TELOPT_SGA: /* this can't hurt */ linemode |= LM_CHAR; tty_special_keys(); sendopt(DO, *s); break; default: /* don't accept other options */ sendopt(DONT, *s); break; } state = old_state; break; case GOTWONT: #ifdef TELOPTS tty_printf("[got WONT %s]\n", TELOPTSTR(*s)); #endif if (*s == TELOPT_ECHO) { /* host no longer echoes, we do it instead */ linemode &= ~LM_NOECHO; } /* accept any WONT */ sendopt(DONT, *s); state = old_state; break; case GOTDO: #ifdef TELOPTS tty_printf("[got DO %s]\n", TELOPTSTR(*s)); #endif switch(*s) { case TELOPT_SGA: linemode |= LM_CHAR; tty_special_keys(); /* FALLTHROUGH */ case TELOPT_TTYPE: sendopt(WILL, *s); break; case TELOPT_NAWS: sendopt(WILL, *s); tcp_write_tty_size(); break; default: /* accept nothing else */ sendopt(WONT, *s); break; } state = old_state; break; case GOTDONT: #ifdef TELOPTS tty_printf("[got DONT %s]\n", TELOPTSTR(*s)); #endif if (*s == TELOPT_SGA) { linemode &= ~LM_CHAR; tty_special_keys(); } sendopt(WONT, *s); state = old_state; break; case GOTSB: if (*s == IAC) { state = GOTSBIAC; } else { if (subchars < MAX_SUBOPT) subopt[subchars++] = *s; } break; case GOTSBIAC: if (*s == IAC) { if (subchars < MAX_SUBOPT) subopt[subchars++] = IAC; state = GOTSB; } else if (*s == SE) { subopt[subchars] = '\0'; dosubopt(subopt); state = old_state; } else { /* problem! I haven't the foggiest idea of what to do here. * I'll just ignore it and hope it goes away. */ PRINTF("#telnet: got IAC <%d> instead of IAC SE!\n", (int)*s); state = old_state; } break; } } CONN_LIST(fd).state = state; CONN_LIST(fd).old_state = old_state; if (!(CONN_LIST(tcp_fd).flags & SPAWN)) { log_write(buffer, (char *)p - buffer, 0); } return (char *)p - buffer; } static void internal_tcp_raw_write(int fd, const char *data, int len) { while (len > 0) { int i; while ((i = write(fd, data, len)) < 0 && errno == EINTR) ; if (i < 0) { errmsg("write to socket"); break; } data += i; len -= i; } } void tcp_raw_write(int fd, const char *data, int len) { tcp_flush(); internal_tcp_raw_write(fd, data, len); } /* write data, escape any IACs */ void tcp_write_escape_iac(int fd, const char *data, int len) { tcp_flush(); for (;;) { const char *iac = memchr(data, IAC, len); size_t l = iac ? (iac - data) + 1 : len; internal_tcp_raw_write(fd, data, l); if (iac == NULL) return; internal_tcp_raw_write(fd, iac, 1); len -= l; data = iac + 1; } } /* * Send current terminal size (RFC 1073) */ void tcp_write_tty_size(void) { static byte buf[] = { IAC, SB, TELOPT_NAWS, 0, 0, 0, 0, IAC, SE }; buf[3] = cols >> 8; buf[4] = cols & 0xff; buf[5] = lines >> 8; buf[6] = lines & 0xff; tcp_raw_write(tcp_main_fd, (char *)buf, 9); #ifdef TELOPTS tty_printf("[sent term size %d %d]\n", cols, lines); #endif } /* * send a string to the main connection on the remote host */ void tcp_main_write(char *data) { tcp_write(tcp_main_fd, data); } static char output_buffer[BUFSIZE]; static int output_len = 0; /* number of characters in output_buffer */ static int output_socket = -1; /* to which socket buffer should be sent*/ /* * put data in the output buffer for transmission to the remote host */ void tcp_write(int fd, char *data) { char *iacs, *out; int len, space, iacp; len = strlen(data); if (tcp_main_fd != -1 && tcp_main_fd == fd) { if (linemode & LM_NOECHO) log_write("", 0, 1); /* log a newline only */ else log_write(data, len, 1); reprint_writeline(data); } /* must be AFTER reprint_writeline() */ if (CONN_LIST(tcp_fd).flags & SPAWN) status(1); else status(-1); if (fd != output_socket) { /* is there data to another socket? */ tcp_flush(); /* then flush it */ output_socket = fd; } out = output_buffer + output_len; space = BUFSIZE - output_len; while (len) { iacs = memchr(data, IAC, len); iacp = iacs ? iacs - data : len; if (iacp == 0) { /* we're at the IAC, send it */ if (space < 2) { tcp_flush(); out = output_buffer; space = BUFSIZE; } *out++ = (char)IAC; *out++ = (char)IAC; output_len += 2; space -= 2; data++; len--; continue; } while (space < iacp) { memcpy(out, data, space); data += space; len -= space; iacp -= space; output_len = BUFSIZE; tcp_flush(); out = output_buffer; space = BUFSIZE; } if (iacp /* && space >= iacp */ ) { memcpy(out, data, iacp); out += iacp; output_len += iacp; space -= iacp; data += iacp; len -= iacp; } } if (!space) { tcp_flush(); out = output_buffer; } *out++ = '\n'; output_len++; } /* * send all buffered data to the remote host */ void tcp_flush(void) { int n; char *p = output_buffer; if (output_len && output_socket == -1) { clear_input_line(1); PRINTF("#no open connections. Use '#connect main
' to open a connection.\n"); output_len = 0; return; } if (!output_len) return; while (output_len) { while ((n = write(output_socket, p, output_len)) < 0 && errno == EINTR) ; if (n < 0) { output_len = 0; errmsg("write to socket"); return; } sent += n; p += n; output_len -= n; } if (CONN_LIST(output_socket).flags & SPAWN) status(1); else /* sent stuff, so we expect a prompt */ status(-1); } /* * Below are multiple-connection support functions: */ /* * return connection's fd given id, * or -1 if null or invalid id is given */ int tcp_find(char *id) { int i; for (i=0; i= MAX_CONNECTS) { PRINTF("#too many open connections.\n"); return; } if (tcp_find(id)>=0) { PRINTF("#connection \"%s\" already open.\n", id); return; } /* find a free slot */ for (i=0; i=0 && !CONN_INDEX(i).id); conn_max_index = i+1; } /* recalculate tcp_max_fd */ for (i = tcp_max_fd = 0; i=0) { CONN_LIST(sfd).flags ^= ACTIVE; if (opt_info) { PRINTF("#connection %s is now %sactive.\n", CONN_LIST(sfd).id, CONN_LIST(sfd).flags & ACTIVE ? "" : "non"); } } else { PRINTF("#no such connection: %s\n", id); } } void tcp_spawn(char *id, char *cmd) { int i, childpid, sockets[2]; if (tcp_find(id)>=0) { PRINTF("#connection \"%s\" already open.\n", id); return; } if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) < 0) { errmsg("create socketpair"); return; } unescape(cmd); switch (childpid = fork()) { case 0: /* child */ close(0); close(1); close(2); setsid(); dup2(sockets[1], 0); dup2(sockets[1], 1); dup2(sockets[1], 2); close(sockets[0]); close(sockets[1]); execl("/bin/sh", "sh", "-c", cmd, NULL); syserr("execl"); break; case -1: close(sockets[0]); close(sockets[1]); errmsg("fork"); return; } close(sockets[1]); /* Again, we don't want children to inherit sockets */ fcntl(sockets[0], F_SETFD, FD_CLOEXEC); /* now find a free slot */ for (i=0; i