/************************************************************************ Realms of Aurealis James Rhone aka Vall of RoA exshell.c External editor code which uses pseudo terminals as a way to spawn off an editing session. Used primarily for builders who need the services of an actual editor. Lots of networking things in here, as well as some good ole fork/exec code. Ahhh, nothing like Unix. :) ******** 100% Completely Original Code ******** *** BE AWARE OF ALL RIGHTS AND RESERVATIONS *** ******** 100% Completely Original Code ******** All rights reserved henceforth. Please note that no guarantees are associated with any code from Realms of Aurealis. All code which has been released to the general public has been done so with an 'as is' pretense. RoA is based on both Diku and CircleMUD and ALL licenses from both *MUST* be adhered to as well as the RoA license. *** Read, Learn, Understand, Improve *** // This code has a bunch of origins, most of which I (-jtrhone) // have got working, but a lot of which has been derived from various // unix networkin books, I do not claim I wrote this from scratch. *************************************************************************/ #define _EXSHELL_C_ #include "sysdep.h" #include "conf.h" #include <sys/socket.h> #include <sys/wait.h> #include <sys/resource.h> #include <netinet/in.h> #include <arpa/telnet.h> #include <arpa/inet.h> #include <netdb.h> #include <signal.h> #include <errno.h> #include <fcntl.h> #include <sys/stat.h> #include <unistd.h> #include "structures.h" #include "utils.h" #include "comm.h" #include "clicomm.h" #include "interpreter.h" #include "db.h" #include "acmd.h" #include "screen.h" #include "roaolc.h" #include "exshell.h" #include "lists.h" #include "global.h" // externals extern int MASTER_DESCRIPTOR; extern int CLIENT_DESCRIPTOR; extern FILE *player_fl; extern void string_add(dsdata *d, char *str, BOOL term); extern void nonblock(int s); // This is RoA's SIGCHLD signal handler, used when builder exits external // editor, the death of it will signal the mud and trigger this function... // // Since the server has other children now besides just editors, add // functionality to take care of them too (client downloads...) // 12/5/97 -jtrhone // // if the cli_router dies, restart it... 8/4/98 -jtrhone RETSIGTYPE grim_reaper(int sig_received) { int status = 0, pid; dsdata *d, *next_d; static char tmpfile[85000]; struct rusage r; #ifdef CLIENT_FORK clpid_info *cl; #endif *tmpfile = '\0'; // could use WNOHANG if we didn't wanna wait pid = wait3(&status, WNOHANG, &r); // for some reason, no more children out there... // usually occurs when a system call returns, no biggie... ignore and reset if (pid == -1) { my_signal(SIGCHLD, grim_reaper); return; } #ifdef DEBUG_MAX sprintf(buf, "SYSUPD: GrimReaper (%d) has fielded PID: %d", getpid(), pid); mudlog(buf, BUG, LEV_IMM, TRUE); #endif // if our router dies, we need to restart it 8/4/98 -jtrhone if (pid == cli_router_pid) { start_cli_router(); my_signal(SIGCHLD, grim_reaper); return; } // return our friend who was in shell to the mud world :) for (d=descriptor_list; d; d=next_d) { next_d = d->next; if (d->rerouted && d->child_pid && d->child_pid == pid) { d->child_pid = -1; d->rerouted = FALSE; // needed so we can start readin their input again if (d->character) { sprintf(buf, "edits/%s.edit",GET_NAME(d->character)); huge_file_to_string(buf, tmpfile); if (!*tmpfile) strcpy(tmpfile, "%1WARNING%0: String not loaded or too long.\n"); string_add(d, tmpfile, TRUE); // TRUE means terminate now #ifdef DEBUG_MAX sprintf(buf, "SYSUPD: GrimReaper (%d) matched ExShell PID: %d", getpid(), pid); mudlog(buf, BUG, LEV_IMM, TRUE); #endif } } } #ifdef CLIENT_FORK // ELSE it's some other dead child... client download perhaps // compare against the global client PID list // if we get a match, free it's associated string 12/5/97 -jtrhone for (cl = cl_pids; cl; cl=cl->next) if (cl->pid == pid) { delete_clpid_entry(cl); #ifdef DEBUG_MAX sprintf(buf, "SYSUPD: GrimReaper (%d) matched clpid PID: %d", getpid(), pid); mudlog(buf, BUG, LEV_IMM, TRUE); #endif break; } #endif // reset handler... my_signal(SIGCHLD, grim_reaper); } // i need a specialized system capability... ignore all system child exits... // 3/6/98 -jtrhone // Hmm, should really block rather than ignore, ah well // ehhh.... we'll just ignore these children in reaper...3/15/98 -jtrhone int roa_system(char *comm) { int retval; // ignore the sigchld... // my_signal(SIGCHLD, SIG_IGN); // call the real system (will fork, exec, wait for it to finish, then return) retval = system(comm); // reinstate the reaper :) // my_signal(SIGCHLD, grim_reaper); // return it to maintain sanity return retval; } int route_io( int fdnet, int fdpty ) { struct timeval sleep_time; fd_set r_set, w_set, o_set; int state = ST_DATA; char netbuf[1024]; char netbuf2[1024]; char ptybuf[1024]; char ptybuf2[2048]; int netbytes = 0; int ptybytes = 0; int maxdesc = fdnet; // note, fdnet == d->descriptor // find our largest descriptor number if (maxdesc < fdpty) maxdesc = fdpty; // clear out all our sets, no descriptors are in them after this FD_ZERO( &r_set ); FD_ZERO( &w_set ); FD_ZERO( &o_set ); // begin the server (mini telnetd) for( ; ; ) { sleep_time.tv_sec = 0; sleep_time.tv_usec = 1; // include the fdnet descriptor in our read, write, and exception sets FD_SET( (int)fdnet, &r_set ); FD_SET( (int)fdnet, &w_set ); FD_SET( (int)fdnet, &o_set ); // include the fdpty descriptor in our read and write sets FD_SET( (int)fdpty, &r_set ); FD_SET( (int)fdpty, &w_set ); // look for anything to be read from 0 - maxdesc select( maxdesc+1, &r_set, 0, 0, &sleep_time ); // keep sitting here until something comes in write set or // exception set, breaks out on reception or error while( select( maxdesc+1, 0, &w_set, &o_set, &sleep_time ) < 0 ) { if( errno == EINTR ) continue; perror( "route_io:select" ); return -1; } // if fdnet is in the readset and we havent read anything yet if( FD_ISSET(fdnet, &r_set) && netbytes == 0 ) { if( ( netbytes = read(fdnet, netbuf, 1024 )) < 0 ) { perror( "route_io:read-from-fdnet" ); return -1; } else if( netbytes == 0 ) if( fdnet < 0 ) break; } // see if fdpty is still around // if not, its not an error, just return for now if (fdpty < 0) return 1; // see if the pty is in the write set, and we have read stuff if( FD_ISSET( (int)fdpty, &w_set ) && netbytes > 0 ) { int i = 0; int j = 0; ubyte ch; // stick our written stuff from fdnet into netbuf2 // translating along the way // we will write netbuf2 to fdpty a bit later while( i < netbytes ) { ch = netbuf[i++] & 0377; switch(state) { case ST_DATA: if( ch == IAC ) { state = ST_IAC; continue; } else if( ch == '\r' ) state = ST_CRLF; netbuf2[j++] = ch; continue; default: case ST_CRLF: // Skip '\n' if '\r\n' state = ST_DATA; if( ch == '\n' || ch == '\0' ) continue; case ST_IAC: // Messy. Eat up IACs for now. if( ch == SB ) state = ST_SE; else if( ch == IP ) // Kludgy! Will handle data-mark and urgent // data later, right now just kill the shell. { exit(0); } else if( ch == IAC ) // double IAC can happen { netbuf2[j++] = ch; state = ST_DATA; } else state = ST_OPT; break; case ST_OPT: case ST_DO: case ST_DONT: case ST_WILL: case ST_WONT: // will/wont/do/dont - discarding for now state = ST_DATA; break; case ST_SE: if( ch == SE ) state = ST_DATA; break; } } // If error assume shell exited // write netbuf2 to the pty, assuming its still open if( write( fdpty, netbuf2, j ) < 0 ) { if (errno == EFAULT) perror( "writing outside of fdpty process space" ); if (errno == EINVAL) perror( "fdpty was negative" ); if (errno == EPIPE) perror( "attempted to write to descriptor which nobody is readin" ); if (errno == EBADF) perror( "attempted to write to invalid descriptor" ); return -1; } netbytes = 0; } // Pty has data // fdpty is in read_set and we havent read anything yet if( FD_ISSET(fdpty, &r_set ) && ptybytes == 0 ) { if( ( ptybytes = read(fdpty, ptybuf, 1024 )) < 0 ) { perror( "fdpty.read" ); return -1; } else if( ptybytes == 0 ) if( fdpty < 0 ) break; } // if our net, fdnet is in write set and we have read stuff from pty if( FD_ISSET( (int)fdnet, &w_set ) && ptybytes > 0 ) { int i = 0; int j = 0; int ch; // This little snippet is from reading Berkeley sources // trying to understand the telnet protocol. // If we were supporting binary mode (RFC 856) we would need // more than what I have. IAC is a legal byte in binary // mode so if we get double IAC we just pass one thru. // --- // Concerning CRLF, since we are the server (UNIX side) // we only expect '\n' so translate for client. If we // see '\r' from server side, ignore since it was probably // part of some other app trying to do translation like us. // This is only my understanding, which is very limited. // stick all our read pty bytes into ptybuf2... // translating along the way while( i < ptybytes ) { ch = ptybuf[i++] & 0377; if( ch == IAC ) ptybuf2[j++] = ch; else if( ch == '\n' ) { ptybuf2[j++] = '\r'; ptybuf2[j++] = ch; } else if( ch != '\r' ) ptybuf2[j++] = ch; } // if we cannot write what we read from the pty to the fdnet // we have a problem :) if( write( fdnet, ptybuf2, j ) < 0 ) { perror( "route_io:write-to-fdnet" ); return -1; } // ok, set our pty # of read bytes back to 0 and loop back ptybytes = 0; } } return 0; } // OK THIS IS THE MINI TELNETD BRANCH, this will take care of routing void run_mini_telnetd(int master, int desc) { // make sure our master doesnt block nonblock(master); // let em know we are offering to do the echoing and we will // suppress any go aheads write(desc, WILL_ECHO, 3); write(desc, WILL_SUPPRESS_GA, 3); // run mini telnetd between pty and mud // note, will not return until the session is over route_io(desc, master); // reset our previous offers after we return from mini telnetd write(desc, WONT_ECHO, 5); write(desc, WONT_SUPPRESS_GA, 3); // when we're done routing, exit this child exit(0); } /* 4.3 BSD */ // BSD ptys are in form ptyXY ( ptyp0 to ptysf ) where // X is p-s and Y is 0-f hex // Increment through until we succeed in opening one. // The slave side of a BSD ptyXY is ttyXY int open_pty_master( char *pty ) { int master_fd; struct stat s; const char * Ychars = "0123456789abcdefghijklmnopqrstuv"; int j; strcpy( pty, "/dev/ptyXY" ); pty[8] = 'a'; // this is only X char allowed on our system, scan if you // have more -roa pty[9] = '0'; // See if system has /dev entry for ptyX0, if not we are done if( stat( pty, &s ) < 0 ) { perror("open master pty cannot find /dev/ptya0:"); return -1; } // just go to u, blah should find an empty pty out of 31 tries -roa for( j=0; j < 30; j++ ) { pty[9] = Ychars[j]; if( ( master_fd = open( pty, O_RDWR ) ) >= 0 ) return master_fd; // grab it and run else { sprintf(buf, "Could not open master pty %s.",pty); mudlog(buf, BUG, LEV_IMM, TRUE); } } return -1; } // Going to have to play with this to scan for next available // slave pty -about to be updated as soon as possible // currently only allows for one person to be shelled out int open_pty_slave( int master_fd, const char *pty ) { int slave_fd; char tty[12]; // uses same the pty string just replaces the p with t // ie. if we grabbed /dev/ptyp5 for master // the slave tries for /dev/ttyp5 -roa strcpy( tty, pty ); tty[5] = 't'; // should we scan here? for an open slave tty -roa if( (slave_fd = open(tty, O_RDWR)) < 0) { close( master_fd ); sprintf(buf, "Could not open slave pty %s.",tty); mudlog(buf, BUG, LEV_IMM, TRUE); return -1; } else return slave_fd; } // do a stat on the thought to be location of the editor... BOOL no_spico(void) { struct stat s; if (stat(spico_path, &s) < 0) { perror("statting: "); return TRUE; } else return FALSE; } // This starts up all the server stuff to be documented much better as I // become more comfortable with it... note: all the following code is // probably very OS specific for the most part, and is only being written // for BSDi systems, perhaps a System V addition sometime in the future // to make it at least semi portable... // for now however, RoA runs on FreeBSD, so I stick with that type OS :) // update: This code has been run successfully on FreeBSD, BSDi, and RH Linux... // Kudos to the MUD++ dev team for giving me something to look at // jtrhone aka Vall RoA int startshell(dsdata *d, char *editor, char *fname) { int master_fd; int slave_fd; char pty[12]; int pid; dsdata *td; // if no editor... mudlog and bail... if (no_spico()) { send_to_char("Unable to locate external editor (check configuration file).\n\r", d->character); mudlog("SYSERR: Unable to locate external editor.", BRF, LEV_IMM, TRUE); return 0; } fflush(NULL); // flush ALL open buffered streams /* fork a server off to take care of shell and IO */ if((pid = fork()) > 0) { // Original parent d->rerouted = TRUE; d->child_pid = pid; return 0; } else if(pid < 0) // error on fork { perror("fork"); perror("system error forking shell.\n\r"); return -1; } // RoA // SERVER BEGINS HERE, child of main mud, this will eventually // split again into two branches: // A) parent branch, a route_io server (mini telnetd) and // B) the exec branch, the branch that gets blasted by the shell // first we have to secure this so it doesnt have access to the main // mud things, like the player_fl fclose(player_fl); // close our main descriptor, or the mother_desc // also close client master now 5/19/98 -jtrhone close(MASTER_DESCRIPTOR); close(CLIENT_DESCRIPTOR); // close every descriptor in the mud except the one were workin with for (td = descriptor_list; td; td = td->next) if (td != d) close(td->descriptor); // make sure files are r/w/e by group umask(0070); // grab a pseudo terminal master if((master_fd = open_pty_master(pty)) < 0 ) { perror( "Failed to open pty master for shell.\n\r" ); return -1; } // WE SPLIT HERE AGAIN, into server & exec branch pid = fork(); if(pid == 0) { // this is the exec branch, it will soon wax everything with an exec // make SURE we will send a SIGCHLD to its parent when we die // thats how grim_reaper knows we have died struct sigaction sa; sa.sa_flags = SA_RESETHAND; sa.sa_handler = grim_reaper; if (sigaction(SIGCHLD, &sa, 0) < 0) { // yuikes, we failed for some reason // send a kill to the parent process of this one (minitelnetd) if ( kill(getppid(), SIGKILL) < 0 ) perror( "open_pty_slave, parent kill:" ); perror("sigaction: cant reset SIGCHLD"); exit(0); } // ok, now have this exec branch grab a slave pty sending our master if((slave_fd = open_pty_slave(master_fd, pty)) < 0) { if ( kill(getppid(), SIGKILL) < 0 ) perror( "open_pty_slave, parent kill:" ); perror( "open_pty_slave:" ); exit(0); } // ok we grabbed a slave, now close the exec branches master as well // as stdin, stdout, and stderr close(master_fd); close(0); close(1); close(2); // ok, we wanna dupe our slave into stdin AND stdout AND stderr if(dup(slave_fd) != 0 || dup(slave_fd) != 1 )// || dup(slave_fd) != 2) { if ( kill(getppid(), SIGKILL) < 0 ) perror( "open_pty_slave, parent kill:" ); perror( "dup failed\n\r" ); exit(0); } // kewl, now the exec branch can only communicate thru this slave // descriptor, we have copies of it in stdin stdout and stderr // so we dont need the original anymore close(slave_fd); // do the exec, replaces entire code with new code, but does not // reset stdin stdout and stderr, so whatever is ran can only // communicate thru the slave_fd chdir("edits"); execl(spico_path, "spico", fname, (char*)0); if ( kill(getppid(), SIGKILL) < 0 ) perror( "open_pty_slave, parent kill:" ); perror( "execl" ); exit(0); } else if( pid < 0 ) // fork error, bail { perror( "fork slave pty failed" ); return -1; } // send this branch into telnet mode... // this exits before it ever returns here.. -roa run_mini_telnetd(master_fd, d->descriptor); // never get here, avoid compiler warning though return -1; } #undef _EXSHELL_C_