xatonum.h: add comment
[oweals/busybox.git] / networking / telnetd.c
index 9ce793fb7fa7a4cddcb424fda9cad81416ec533e..cccf03dfd54944bd88ff059eab52392117511db5 100644 (file)
 struct tsession {
        struct tsession *next;
        int sockfd_read, sockfd_write, ptyfd;
-       /*int shell_pid;*/
+       int shell_pid;
 
        /* two circular buffers */
        /*char *buf1, *buf2;*/
 /*#define TS_BUF1 ts->buf1*/
 /*#define TS_BUF2 TS_BUF2*/
-#define TS_BUF1 ((char*)(ts + 1))
-#define TS_BUF2 (((char*)(ts + 1)) + BUFSIZE)
+#define TS_BUF1 ((unsigned char*)(ts + 1))
+#define TS_BUF2 (((unsigned char*)(ts + 1)) + BUFSIZE)
        int rdidx1, wridx1, size1;
        int rdidx2, wridx2, size2;
 };
@@ -82,27 +82,30 @@ static const char *issuefile = "/etc/issue.net";
    FIXME - if we mean to send 0xFF to the terminal then it will be escaped,
    what is the escape character?  We aren't handling that situation here.
 
-   CR-LF ->'s CR mapping is also done here, for convenience
+   CR-LF ->'s CR mapping is also done here, for convenience.
+
+   NB: may fail to remove iacs which wrap around buffer!
  */
-static char *
+static unsigned char *
 remove_iacs(struct tsession *ts, int *pnum_totty)
 {
-       unsigned char *ptr0 = (unsigned char *)TS_BUF1 + ts->wridx1;
+       unsigned char *ptr0 = TS_BUF1 + ts->wridx1;
        unsigned char *ptr = ptr0;
        unsigned char *totty = ptr;
        unsigned char *end = ptr + MIN(BUFSIZE - ts->wridx1, ts->size1);
-       int processed;
        int num_totty;
 
        while (ptr < end) {
                if (*ptr != IAC) {
-                       int c = *ptr;
-                       *totty++ = *ptr++;
+                       char c = *ptr;
+
+                       *totty++ = c;
+                       ptr++;
                        /* We now map \r\n ==> \r for pragmatic reasons.
                         * Many client implementations send \r\n when
                         * the user hits the CarriageReturn key.
                         */
-                       if (c == '\r' && (*ptr == '\n' || *ptr == 0) && ptr < end)
+                       if (c == '\r' && ptr < end && (*ptr == '\n' || *ptr == '\0'))
                                ptr++;
                } else {
                        /*
@@ -120,6 +123,7 @@ remove_iacs(struct tsession *ts, int *pnum_totty)
                         */
                        else if (ptr[1] == SB && ptr[2] == TELOPT_NAWS) {
                                struct winsize ws;
+
                                if ((ptr+8) >= end)
                                        break;  /* incomplete, can't process */
                                ws.ws_col = (ptr[3] << 8) | ptr[4];
@@ -137,16 +141,15 @@ remove_iacs(struct tsession *ts, int *pnum_totty)
                }
        }
 
-       processed = ptr - ptr0;
        num_totty = totty - ptr0;
-       /* the difference between processed and num_to tty
-          is all the iacs we removed from the stream.
-          Adjust buf1 accordingly. */
-       ts->wridx1 += processed - num_totty;
-       ts->size1 -= processed - num_totty;
        *pnum_totty = num_totty;
-       /* move the chars meant for the terminal towards the end of the
-       buffer. */
+       /* the difference between ptr and totty is number of iacs
+          we removed from the stream. Adjust buf1 accordingly. */
+       if ((ptr - totty) == 0) /* 99.999% of cases */
+               return ptr0;
+       ts->wridx1 += ptr - totty;
+       ts->size1 -= ptr - totty;
+       /* move chars meant for the terminal towards the end of the buffer */
        return memmove(ptr - num_totty, ptr0, num_totty);
 }
 
@@ -200,7 +203,7 @@ getpty(char *line, int size)
 
 static struct tsession *
 make_new_session(
-               USE_FEATURE_TELNETD_STANDALONE(int sock_r, int sock_w)
+               USE_FEATURE_TELNETD_STANDALONE(int sock)
                SKIP_FEATURE_TELNETD_STANDALONE(void)
 ) {
        const char *login_argv[2];
@@ -218,19 +221,23 @@ make_new_session(
                bb_error_msg("can't create pty");
                return NULL;
        }
-       if (fd > maxfd) maxfd = fd;
+       if (fd > maxfd)
+               maxfd = fd;
        ts->ptyfd = fd;
        ndelay_on(fd);
 #if ENABLE_FEATURE_TELNETD_STANDALONE
-       if (sock_w > maxfd) maxfd = sock_w;
-       ts->sockfd_write = sock_w;
-       ndelay_on(sock_w);
-       if (sock_r > maxfd) maxfd = sock_r;
-       ts->sockfd_read = sock_r;
-       ndelay_on(sock_r);
+       ts->sockfd_read = sock;
+       ndelay_on(sock);
+       if (!sock) { /* We are called with fd 0 - we are in inetd mode */
+               sock++; /* so use fd 1 for output */
+               ndelay_on(sock);
+       }
+       ts->sockfd_write = sock;
+       if (sock > maxfd)
+               maxfd = sock;
 #else
-       ts->sockfd_write = 1;
        /* ts->sockfd_read = 0; - done by xzalloc */
+       ts->sockfd_write = 1;
        ndelay_on(0);
        ndelay_on(1);
 #endif
@@ -256,13 +263,13 @@ make_new_session(
        if (pid < 0) {
                free(ts);
                close(fd);
-               /* sock_r and sock_w will be closed by caller */
+               /* sock will be closed by caller */
                bb_perror_msg("vfork");
                return NULL;
        }
        if (pid > 0) {
                /* Parent */
-               /*ts->shell_pid = pid;*/
+               ts->shell_pid = pid;
                return ts;
        }
 
@@ -302,10 +309,19 @@ make_new_session(
        login_argv[0] = loginpath;
        login_argv[1] = NULL;
        execvp(loginpath, (char **)login_argv);
-       /* Safer with vfork, and we shouldn't send this to the client anyway */
+       /* Safer with vfork, and we shouldn't send message
+        * to remote clients anyway */
        _exit(1); /*bb_perror_msg_and_die("execv %s", loginpath);*/
 }
 
+/* Must match getopt32 string */
+enum {
+       OPT_WATCHCHILD = (1 << 2), /* -K */
+       OPT_INETD      = (1 << 3) * ENABLE_FEATURE_TELNETD_STANDALONE, /* -i */
+       OPT_PORT       = (1 << 4) * ENABLE_FEATURE_TELNETD_STANDALONE, /* -p */
+       OPT_FOREGROUND = (1 << 6) * ENABLE_FEATURE_TELNETD_STANDALONE, /* -F */
+};
+
 #if ENABLE_FEATURE_TELNETD_STANDALONE
 
 static void
@@ -313,6 +329,9 @@ free_session(struct tsession *ts)
 {
        struct tsession *t = sessions;
 
+       if (option_mask32 & OPT_INETD)
+               exit(0);
+
        /* Unlink this telnet session from the session list */
        if (t == ts)
                sessions = ts->next;
@@ -322,45 +341,69 @@ free_session(struct tsession *ts)
                t->next = ts->next;
        }
 
+#if 0
        /* It was said that "normal" telnetd just closes ptyfd,
         * doesn't send SIGKILL. When we close ptyfd,
         * kernel sends SIGHUP to processes having slave side opened. */
-       /*kill(ts->shell_pid, SIGKILL);
-       wait4(ts->shell_pid, NULL, 0, NULL);*/
+       kill(ts->shell_pid, SIGKILL);
+       wait4(ts->shell_pid, NULL, 0, NULL);
+#endif
        close(ts->ptyfd);
        close(ts->sockfd_read);
-       /* error if ts->sockfd_read == ts->sockfd_write. So what? ;) */
-       close(ts->sockfd_write);
+       /* We do not need to close(ts->sockfd_write), it's the same
+        * as sockfd_read unless we are in inetd mode. But in inetd mode
+        * we do not reach this */
        free(ts);
 
        /* Scan all sessions and find new maxfd */
-       ts = sessions;
        maxfd = 0;
+       ts = sessions;
        while (ts) {
                if (maxfd < ts->ptyfd)
                        maxfd = ts->ptyfd;
                if (maxfd < ts->sockfd_read)
                        maxfd = ts->sockfd_read;
+#if 0
+               /* Again, sockfd_write == sockfd_read here */
                if (maxfd < ts->sockfd_write)
                        maxfd = ts->sockfd_write;
+#endif
                ts = ts->next;
        }
 }
 
 #else /* !FEATURE_TELNETD_STANDALONE */
 
-/* Never actually called */
-void free_session(struct tsession *ts);
+/* Used in main() only, thus exits. */
+#define free_session(ts) return 0
 
 #endif
 
+static void handle_sigchld(int sig)
+{
+       pid_t pid;
+       struct tsession *ts;
+
+       pid = waitpid(-1, &sig, WNOHANG);
+       if (pid > 0) {
+               ts = sessions;
+               while (ts) {
+                       if (ts->shell_pid == pid) {
+                               ts->shell_pid = -1;
+                               return;
+                       }
+                       ts = ts->next;
+               }
+       }
+}
+
 
 int telnetd_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
 int telnetd_main(int argc, char **argv)
 {
        fd_set rdfdset, wrfdset;
        unsigned opt;
-       int selret, maxlen, w, r;
+       int count;
        struct tsession *ts;
 #if ENABLE_FEATURE_TELNETD_STANDALONE
 #define IS_INETD (opt & OPT_INETD)
@@ -375,30 +418,29 @@ int telnetd_main(int argc, char **argv)
                portnbr = 23,
        };
 #endif
-       enum {
-               OPT_INETD      = (1 << 2) * ENABLE_FEATURE_TELNETD_STANDALONE, /* -i */
-               OPT_PORT       = (1 << 3) * ENABLE_FEATURE_TELNETD_STANDALONE, /* -p */
-               OPT_FOREGROUND = (1 << 5) * ENABLE_FEATURE_TELNETD_STANDALONE, /* -F */
-       };
-
-       /* If !STANDALONE, we accept (and ignore) -i, thus people
+       /* Even if !STANDALONE, we accept (and ignore) -i, thus people
         * don't need to guess whether it's ok to pass -i to us */
-       opt = getopt32(argv, "f:l:i" USE_FEATURE_TELNETD_STANDALONE("p:b:F"),
+       opt = getopt32(argv, "f:l:Ki" USE_FEATURE_TELNETD_STANDALONE("p:b:F"),
                        &issuefile, &loginpath
                        USE_FEATURE_TELNETD_STANDALONE(, &opt_portnbr, &opt_bindaddr));
+       if (!IS_INETD /*&& !re_execed*/) {
+               /* inform that we start in standalone mode?
+                * May be useful when people forget to give -i */
+               /*bb_error_msg("listening for connections");*/
+               if (!(opt & OPT_FOREGROUND)) {
+                       /* DAEMON_CHDIR_ROOT was giving inconsistent
+                        * behavior with/without -F, -i */
+                       bb_daemonize_or_rexec(0 /*DAEMON_CHDIR_ROOT*/, argv);
+               }
+       }
        /* Redirect log to syslog early, if needed */
        if (IS_INETD || !(opt & OPT_FOREGROUND)) {
                openlog(applet_name, 0, LOG_USER);
                logmode = LOGMODE_SYSLOG;
        }
-       //if (opt & 1) // -f
-       //if (opt & 2) // -l
        USE_FEATURE_TELNETD_STANDALONE(
-               if (opt & OPT_PORT) // -p
+               if (opt & OPT_PORT)
                        portnbr = xatou16(opt_portnbr);
-               //if (opt & 8) // -b
-               //if (opt & 0x10) // -F
-               //if (opt & 0x20) // -i
        );
 
        /* Used to check access(loginpath, X_OK) here. Pointless.
@@ -406,16 +448,12 @@ int telnetd_main(int argc, char **argv)
 
 #if ENABLE_FEATURE_TELNETD_STANDALONE
        if (IS_INETD) {
-               sessions = make_new_session(0, 1);
+               sessions = make_new_session(0);
                if (!sessions) /* pty opening or vfork problem, exit */
                        return 1; /* make_new_session prints error message */
        } else {
-//vda: inform that we start in standalone mode?
                master_fd = create_and_bind_stream_or_die(opt_bindaddr, portnbr);
                xlisten(master_fd, 1);
-               if (!(opt & OPT_FOREGROUND))
-//vda: NOMMU?
-                       bb_daemonize(DAEMON_CHDIR_ROOT);
        }
 #else
        sessions = make_new_session();
@@ -426,6 +464,9 @@ int telnetd_main(int argc, char **argv)
        /* We don't want to die if just one session is broken */
        signal(SIGPIPE, SIG_IGN);
 
+       if (opt & OPT_WATCHCHILD)
+               signal(SIGCHLD, handle_sigchld);
+
 /*
    This is how the buffers are used. The arrows indicate the movement
    of data.
@@ -447,14 +488,6 @@ int telnetd_main(int argc, char **argv)
  again:
        FD_ZERO(&rdfdset);
        FD_ZERO(&wrfdset);
-       if (!IS_INETD) {
-               FD_SET(master_fd, &rdfdset);
-               /* This is needed because free_session() does not
-                * take into account master_fd when it finds new
-                * maxfd among remaining fd's: */
-               if (master_fd > maxfd)
-                       maxfd = master_fd;
-       }
 
        /* Select on the master socket, all telnet sockets and their
         * ptys if there is room in their session buffers.
@@ -462,19 +495,33 @@ int telnetd_main(int argc, char **argv)
         * before each select. Can be a problem with 500+ connections. */
        ts = sessions;
        while (ts) {
-               if (ts->size1 > 0)       /* can write to pty */
-                       FD_SET(ts->ptyfd, &wrfdset);
-               if (ts->size1 < BUFSIZE) /* can read from socket */
-                       FD_SET(ts->sockfd_read, &rdfdset);
-               if (ts->size2 > 0)       /* can write to socket */
-                       FD_SET(ts->sockfd_write, &wrfdset);
-               if (ts->size2 < BUFSIZE) /* can read from pty */
-                       FD_SET(ts->ptyfd, &rdfdset);
-               ts = ts->next;
+               struct tsession *next = ts->next; /* in case we free ts. */
+               if (ts->shell_pid == -1) {
+                       /* Child died ad we detected that */
+                       free_session(ts);
+               } else {
+                       if (ts->size1 > 0)       /* can write to pty */
+                               FD_SET(ts->ptyfd, &wrfdset);
+                       if (ts->size1 < BUFSIZE) /* can read from socket */
+                               FD_SET(ts->sockfd_read, &rdfdset);
+                       if (ts->size2 > 0)       /* can write to socket */
+                               FD_SET(ts->sockfd_write, &wrfdset);
+                       if (ts->size2 < BUFSIZE) /* can read from pty */
+                               FD_SET(ts->ptyfd, &rdfdset);
+               }
+               ts = next;
+       }
+       if (!IS_INETD) {
+               FD_SET(master_fd, &rdfdset);
+               /* This is needed because free_session() does not
+                * take into account master_fd when it finds new
+                * maxfd among remaining fd's */
+               if (master_fd > maxfd)
+                       maxfd = master_fd;
        }
 
-       selret = select(maxfd + 1, &rdfdset, &wrfdset, NULL, NULL);
-       if (selret < 0)
+       count = select(maxfd + 1, &rdfdset, &wrfdset, NULL, NULL);
+       if (count < 0)
                goto again; /* EINTR or ENOMEM */
 
 #if ENABLE_FEATURE_TELNETD_STANDALONE
@@ -487,7 +534,7 @@ int telnetd_main(int argc, char **argv)
                if (fd < 0)
                        goto again;
                /* Create a new session and link it into our active list */
-               new_ts = make_new_session(fd, fd);
+               new_ts = make_new_session(fd);
                if (new_ts) {
                        new_ts->next = sessions;
                        sessions = new_ts;
@@ -504,46 +551,42 @@ int telnetd_main(int argc, char **argv)
 
                if (/*ts->size1 &&*/ FD_ISSET(ts->ptyfd, &wrfdset)) {
                        int num_totty;
-                       char *ptr;
+                       unsigned char *ptr;
                        /* Write to pty from buffer 1. */
                        ptr = remove_iacs(ts, &num_totty);
-                       w = safe_write(ts->ptyfd, ptr, num_totty);
-                       if (w < 0) {
+                       count = safe_write(ts->ptyfd, ptr, num_totty);
+                       if (count < 0) {
                                if (errno == EAGAIN)
                                        goto skip1;
-                               if (IS_INETD)
-                                       return 0;
-                               free_session(ts);
-                               ts = next;
-                               continue;
+                               goto kill_session;
                        }
-                       ts->size1 -= w;
-                       ts->wridx1 += w;
+                       ts->size1 -= count;
+                       ts->wridx1 += count;
                        if (ts->wridx1 >= BUFSIZE) /* actually == BUFSIZE */
                                ts->wridx1 = 0;
                }
  skip1:
                if (/*ts->size2 &&*/ FD_ISSET(ts->sockfd_write, &wrfdset)) {
                        /* Write to socket from buffer 2. */
-                       maxlen = MIN(BUFSIZE - ts->wridx2, ts->size2);
-                       w = safe_write(ts->sockfd_write, TS_BUF2 + ts->wridx2, maxlen);
-                       if (w < 0) {
+                       count = MIN(BUFSIZE - ts->wridx2, ts->size2);
+                       count = safe_write(ts->sockfd_write, TS_BUF2 + ts->wridx2, count);
+                       if (count < 0) {
                                if (errno == EAGAIN)
                                        goto skip2;
-                               if (IS_INETD)
-                                       return 0;
-                               free_session(ts);
-                               ts = next;
-                               continue;
+                               goto kill_session;
                        }
-                       ts->size2 -= w;
-                       ts->wridx2 += w;
+                       ts->size2 -= count;
+                       ts->wridx2 += count;
                        if (ts->wridx2 >= BUFSIZE) /* actually == BUFSIZE */
                                ts->wridx2 = 0;
                }
  skip2:
-#if 0
-               /* Not strictly needed, but allows for bigger reads in common case */
+               /* Should not be needed, but... remove_iacs is actually buggy
+                * (it cannot process iacs which wrap around buffer's end)!
+                * Since properly fixing it requires writing bigger code,
+                * we rely instead on this code making it virtually impossible
+                * to have wrapped iac (people don't type at 2k/second).
+                * It also allows for bigger reads in common case. */
                if (ts->size1 == 0) {
                        ts->rdidx1 = 0;
                        ts->wridx1 = 0;
@@ -552,51 +595,47 @@ int telnetd_main(int argc, char **argv)
                        ts->rdidx2 = 0;
                        ts->wridx2 = 0;
                }
-#endif
+
                if (/*ts->size1 < BUFSIZE &&*/ FD_ISSET(ts->sockfd_read, &rdfdset)) {
                        /* Read from socket to buffer 1. */
-                       maxlen = MIN(BUFSIZE - ts->rdidx1, BUFSIZE - ts->size1);
-                       r = safe_read(ts->sockfd_read, TS_BUF1 + ts->rdidx1, maxlen);
-                       if (r <= 0) {
-                               if (r < 0 && errno == EAGAIN)
+                       count = MIN(BUFSIZE - ts->rdidx1, BUFSIZE - ts->size1);
+                       count = safe_read(ts->sockfd_read, TS_BUF1 + ts->rdidx1, count);
+                       if (count <= 0) {
+                               if (count < 0 && errno == EAGAIN)
                                        goto skip3;
-                               if (IS_INETD)
-                                       return 0;
-                               free_session(ts);
-                               ts = next;
-                               continue;
+                               goto kill_session;
                        }
                        /* Ignore trailing NUL if it is there */
-                       if (!TS_BUF1[ts->rdidx1 + r - 1]) {
-                               if (!--r)
-                                       goto skip3;
+                       if (!TS_BUF1[ts->rdidx1 + count - 1]) {
+                               --count;
                        }
-                       ts->size1 += r;
-                       ts->rdidx1 += r;
+                       ts->size1 += count;
+                       ts->rdidx1 += count;
                        if (ts->rdidx1 >= BUFSIZE) /* actually == BUFSIZE */
                                ts->rdidx1 = 0;
                }
  skip3:
                if (/*ts->size2 < BUFSIZE &&*/ FD_ISSET(ts->ptyfd, &rdfdset)) {
                        /* Read from pty to buffer 2. */
-                       maxlen = MIN(BUFSIZE - ts->rdidx2, BUFSIZE - ts->size2);
-                       r = safe_read(ts->ptyfd, TS_BUF2 + ts->rdidx2, maxlen);
-                       if (r <= 0) {
-                               if (r < 0 && errno == EAGAIN)
+                       count = MIN(BUFSIZE - ts->rdidx2, BUFSIZE - ts->size2);
+                       count = safe_read(ts->ptyfd, TS_BUF2 + ts->rdidx2, count);
+                       if (count <= 0) {
+                               if (count < 0 && errno == EAGAIN)
                                        goto skip4;
-                               if (IS_INETD)
-                                       return 0;
-                               free_session(ts);
-                               ts = next;
-                               continue;
+                               goto kill_session;
                        }
-                       ts->size2 += r;
-                       ts->rdidx2 += r;
+                       ts->size2 += count;
+                       ts->rdidx2 += count;
                        if (ts->rdidx2 >= BUFSIZE) /* actually == BUFSIZE */
                                ts->rdidx2 = 0;
                }
  skip4:
                ts = next;
+               continue;
+ kill_session:
+               free_session(ts);
+               ts = next;
        }
+
        goto again;
 }