/* ** j###t ########## #### #### ** j###t ########## #### #### ** j###T "###L J###" ** ######P' ########## ######### ** ######k, ########## T######T ** ####~###L #### ** #### q###L ########## .##### ** #### \###L ########## #####" */ package key; import java.net.*; import java.io.*; import java.util.Vector; import java.util.Hashtable; import java.util.StringTokenizer; import java.util.Enumeration; /** * Works closely with the 'Terminal' class * to achieve different terminal emulations * * Implements RFCs: 1091, 858, 857, 854, 885, 1143, * 855, 1073 */ public final class TelnetIC extends SocketIC { // telnet commands static final int IAC=255; static final int DONT=254; static final int DO=253; static final int WONT=252; static final int WILL=251; static final int SB=250; static final int GA=249; static final int EL=248; static final int EC=247; static final int AYT=246; static final int AO=245; static final int IP=244; static final int BREAK=243; static final int DM=242; static final int NOP=241; static final int SE=240; static final int EOR=239; static final int ABORT=238; static final int SUSP=237; static final int xEOF=236; public static final int SMALLEST_BELIEVABLE_WIDTH = 20; public static final int SMALLEST_BELIEVABLE_HEIGHT = 6; public static final int LARGEST_BELIEVABLE_WIDTH = 140; public static final int LARGEST_BELIEVABLE_HEIGHT = 90; public static final int DEFAULT_WIDTH = 79; public static final int DEFAULT_HEIGHT = 25; /** * The amount of additional space the pager reserves * at the bottom of the screen. 3 allows one blank * line and a message line, without forcing a scroll. */ public static final int PAGER_VERTICAL_SPACE = 5; /** * The number of lines less than the terminal height * which is the border between paging and not paging */ public static final int PAGER_THRESHOLD = 2; public static final String PAGER_LACKS_PAGES = "No more pages to view.\n"; /** * A vector of strings containing lines that are * waiting to be output by the pager. */ private Vector pageBuffer; /** * The top of the _next_ screen to be displayed, * according to the pager. */ private int topOfScreen; private char[] inBuffer; // size of the window int width; int height; private Hashtable options; private TelnetOption teloptEcho; private TelnetOption teloptEOR; private TelnetOption teloptNAWS; private TelnetOption teloptLinemode; private TelnetOption teloptSubliminal; private TelnetOption teloptTerminalType; private Terminal terminal; /** * This is used to hold the autodetected * terminal when the terminal is 'forced' * by the player */ private Terminal savedTerminal; /** true iff the user allows us to use the pager */ boolean canPageEver; /** * True iff the user allows us to word wrap. Turning * this off means that newlines will not be manually * inserted in paragraphs. */ boolean wordwrap = true; /** *-1 if disabled, a value if the user has forced his * terminal to a particular width. */ int forced_width = -1; /** *-1 if disabled, a value if the user has forced his * terminal to a particular height. */ int forced_height = -1; public static final String CRLF = "\r\n"; public static final int INPUT_BUFFER = 640; public static final int MAX_OVERRUN = 640; /** * Constructor * * @param s The socket that the connection is on */ public TelnetIC( Socket s ) throws IOException { super( s ); inBuffer = new char[INPUT_BUFFER+2]; options = new Hashtable( 5 ); terminal = new Terminal(); canPageEver = true; teloptEcho = new EchoTelnetOption( TelnetOption.TELOPT_ECHO, false, true ); options.put( new Integer( TelnetOption.TELOPT_ECHO ), teloptEcho ); teloptEOR = new TelnetOption( TelnetOption.TELOPT_EOR, false, false ); options.put( new Integer( TelnetOption.TELOPT_EOR ), teloptEOR ); teloptNAWS = new NAWSTelnetOption( TelnetOption.TELOPT_NAWS, true, false ); options.put( new Integer( TelnetOption.TELOPT_NAWS ), teloptNAWS ); teloptSubliminal = new TelnetOption( TelnetOption.TELOPT_SUBLIMINAL, true, false ); options.put( new Integer( TelnetOption.TELOPT_SUBLIMINAL ), teloptSubliminal ); teloptTerminalType = new TerminalTypeTelnetOption( this, TelnetOption.TELOPT_TTYPE, true, false ); options.put( new Integer( TelnetOption.TELOPT_TTYPE ), teloptTerminalType ); try { teloptNAWS.sendDo( output ); teloptTerminalType.sendDo( output ); } catch( IOException e ) { //close(); //throw new NetworkException( e ); } width = DEFAULT_WIDTH; height = DEFAULT_HEIGHT; // since we don't have a null constructor, we can't // be created by the factory: this is our concession. Factory.postCreateProcess( this ); } /** * beeps the terminal, if supported. */ public void beep() { try { output.write( terminal.beep() ); output.flush(); } catch( IOException e ) { //close(); //throw new NetworkException( e ); } } /** * Returns true if sucessful */ private boolean setTerminal( String terminalName ) { Terminal newTerm; try { newTerm = Terminal.createTerminal( terminalName ); } catch( IllegalAccessException e ) { throw new UnexpectedResult( e.toString() ); } catch( InstantiationException e ) { throw new UnexpectedResult( e.toString() ); } if( newTerm != null ) { terminal = newTerm; return( true ); } return( false ); } public boolean setDetectedTerminal( String name ) { if( savedTerminal == null ) return( setTerminal( name ) ); else { Terminal s; s = terminal; terminal = savedTerminal; boolean r = setTerminal( name ); savedTerminal = terminal; terminal = s; return( r ); } } /** * Terminals can be autodetected, or overridden by 'forcing' * them (using, incidently, this routine) */ public boolean forceTerminal( String name ) { if( savedTerminal == null ) savedTerminal = terminal; return( setTerminal( name ) ); } public void setWordwrap( boolean v ) { wordwrap = v; } public boolean getWordwrap() { return( wordwrap ); } public void resetTerminal() { if( savedTerminal != null ) { terminal = savedTerminal; savedTerminal = null; } } public Terminal getTerminal() { return( terminal ); } /** * Reads a full (16-bit) integer * from the inputStream, taking * doubled IAC's into account */ private void write16Int( int t ) throws IOException { output.write( t >> 8 ); if( t >> 8 == 255 ) output.write( 255 ); output.write( t ); if( (t & 0x00ff) == 255 ) output.write( 255 ); } // META: recently changed >> 16's to >> 8's in this protocol /** * Reads a full (16-bit) integer * from the inputStream, taking * doubled IAC's into account */ private int read16Int() throws IOException,IACRecievedException { int number=0; int i; int j; for( int k=0; k<2; k++ ) { i = readInt(); if( i == TelnetIC.IAC ) { j = readInt(); if( j == IAC ) { // its the number 255 number = number << 8; number += j; } else { // authentic IAC code - now we're in trouble throw new IACRecievedException( j ); } } else { number = number << 8; number += i; } } return( number ); } private String readIACSETerminatedString() throws IOException { int where=0; char[] buffer = new char[ 40 ]; char b=' '; boolean cont = true; do { b = (char) readInt(); if( b == IAC ) { // handle IAC codes switch( handleIAC() ) { case SE: cont = false; break; } } else if( b == '\n' || b == '\r' ) cont = false; else buffer[where++] = (char) b; } while( cont ); return( new String( buffer, 0, where ) ); } private final int readInt() throws IOException { do { try { int i = inputStream.read(); if( i == -1 ) { close(); throw new NetworkException( "connection closed" ); } return( i ); } catch( SocketException e ) { //System.err.println( Thread.currentThread().getName() + "ignoring: " + e.toString() ); //e.printStackTrace(); } catch( IOException e ) { throw e; } } while( true ); } /** * An IAC code was just recieved down the input stream, and we've been * called to 'take care' of it. ;) */ private int handleIAC() throws IOException { char b; b = (char) readInt(); switch( b ) { case IAC: // a doubled IAC code should be sent direct, but // we should filter it anyway return 0; case SB: // start of a subnegotiation b = (char) readInt(); switch( b ) { case TelnetOption.TELOPT_NAWS: teloptNAWS.negotiation( this ); break; case TelnetOption.TELOPT_TTYPE: teloptTerminalType.negotiation( this ); default: break; } break; case SE: return( SE ); case WILL: case WONT: case DO: case DONT: // regardless of what it is, its still specifying an option next int oc; // the option code oc = readInt(); TelnetOption to = null; switch( oc ) { case TelnetOption.TELOPT_ECHO: to = teloptEcho; break; case TelnetOption.TELOPT_EOR: to = teloptEOR; break; case TelnetOption.TELOPT_NAWS: to = teloptNAWS; break; case TelnetOption.TELOPT_TTYPE: to = teloptTerminalType; break; default: Integer toOptCode = new Integer( oc ); to = (TelnetOption) options.get( toOptCode ); if( to == null ) { // need to create this option to = new TelnetOption( oc, false, false ); options.put( toOptCode, to ); } } switch( b ) { case WILL: to.rcptWill( output ); break; case WONT: to.rcptWont( output ); break; case DO: to.rcptDo( output ); break; case DONT: to.rcptDont( output ); } break; case EC: return( EC ); case EL: return( EL ); default: break; } return 0; } private boolean pushbacked = false; /** * This function will be called every 'now and * again', as a 'check' function when no input * is required (we're not waiting), but some * is allowed. This is useful for the telnet * protocol in particular, which can recieve * IAC's before the login prompt. This function * should be harmless, no matter when its called */ public void check() throws IOException { if( !pushbacked ) { int a = inputStream.available(); if( a > 0 ) { int i; i = readInt(); if( i == IAC ) { handleIAC(); } else { inputStream.unread( i ); pushbacked = true; } } } } public void flushIfWaiting() { try { if( isWaiting > 0 ) output.flush(); } catch( IOException e ) { throw new NetworkException( e ); } } private boolean hiddenMode = false; private boolean hiddenStatus = false; private int isWaiting = 0; private int lastCharWas = -1; /** * This function blocks until a line of input is * recieved. Think of it as being much like the * input statement in BASIC of old. *shudder* * * @param prompt The prompt to give the player */ public String input( String prompt ) { try { isWaiting++; int where=0; char b=' '; if( hiddenStatus && !hiddenMode ) { teloptEcho.sendWont( output ); hiddenStatus = false; } paragraph( prompt, 0, 0, 0, 0, true, false, false ); output.write( IAC ); output.write( (char) GA ); output.flush(); do { // continuously skip until a CR or LF // when the buffer runs out if( where >= INPUT_BUFFER ) { int count = 0; do { try { b = (char) readInt(); count++; } catch( InterruptedIOException e ) { throw new NetworkException( e ); } } while( !( b == '\r' || b == '\n' ) && (count < MAX_OVERRUN) ); if( count == MAX_OVERRUN ) close(); } else { boolean ni = false; do { try { b = (char) readInt(); ni = false; } catch( InterruptedIOException e ) { ni = true; } } while( ni ); } if( b >= 32 && b <= 126 ) // valid ascii characters only { inBuffer[where++] = (char) b; if( !hiddenMode && teloptEcho.enabledUs() ) output.write( b ); } else if( b == 8 || b == 127 ) { if( where > 0 ) where--; if( !hiddenMode && teloptEcho.enabledUs() ) output.write( 8 ); } else if( b == IAC ) { // handle IAC codes switch( handleIAC() ) { case EL: // erase the line where = 0; break; case EC: // erase last character if( where > 0 ) where--; break; case SE: Log.debug( this, "ERROR: unexpected SE" ); break; } } } while( !((b == '\r') && !(lastCharWas == '\n')) && !((b == '\n') && !(lastCharWas == '\r' )) ); lastCharWas = b; //Log.debug( this, "exit loop ( new lastChar is " + (char)b + " )" ); unIdle(); pushbacked = false; return( new String( inBuffer, 0, where ) ); } catch( IOException e ) { //close(); //System.out.println( e.toString() ); //throw new NetworkException( e ); return( "" ); } finally { isWaiting--; } } /** * This function is very similar to * input(), above, except that it should be * used to *not* echo text as it's entered, * mainly for passwords */ public String hiddenInput( String prompt ) { if( !hiddenStatus ) { try { teloptEcho.sendWill( output ); hiddenStatus = true; } catch( IOException e ) { //close(); //throw new NetworkException( e ); } } hiddenMode = true; String i = input( prompt ); hiddenMode = false; // this is an unavoidable little hack. // since, when they hit return while not echo'ing, // the telnet program *won't* echo their return, and // it won't start a new line. However, we always want // it to do this. What we can't detect is that TinyFugue, // a popular client, is linemode based, and will start // a new line on its own accord. This means that, when // using hiddenInput under the TF client, you'll get // double spacing after each call. This isn't really // our fault, but it might seem to be, so here's my // disclaimer. ;) blankLine(); return( i ); } public void sendLine() { try { output.write( dashes( width ) ); terminal.reset( output ); output.write( CRLF ); } catch( IOException e ) { //close(); //throw new NetworkException( e ); } } public void sendRaw( String message ) { try { output.write( message ); output.write( CRLF ); } catch( IOException e ) { //close(); //throw new NetworkException( e ); } } public void send( String message ) { try { paragraph( message, 3, 0, -3, 0, true, false, true ); terminal.reset( output ); output.write( CRLF ); } catch( IOException e ) { //close(); //throw new NetworkException( e ); } } public void send( char qualifier, String message ) { try { paragraph( String.valueOf( qualifier ) + " " + message, 0, 0, 0, 0, false, false, true ); terminal.reset( output ); output.write( CRLF ); } catch( IOException e ) { //close(); //throw new NetworkException( e ); } } protected void sendTextParagraph( int li, int ri, int lfi, int rfi, String message, int alignment, boolean appending, boolean okayToPage ) throws IOException { if( message.length() > 0 ) { switch( alignment ) { case TextParagraph.CENTERALIGNED: { // this option doesn't use these - set to // 0 for the paragraph call lfi = 0; rfi = 0; // calculate the longest line int longestLine=0; StringTokenizer st = new StringTokenizer( message, "\n" ); while( st.hasMoreTokens() ) longestLine = Math.max( longestLine, stringLength( st.nextToken() ) ); // the width of the screen is made smaller by the indents int i = (( (width - li - ri) - longestLine) / 2)-1; if( i <= 0 ) { // try to center it on the normal screen... i = ((width - longestLine) / 2); if( i <= 0 ) li = 0; else li = i; } else li += i; paragraph( message, li, ri, lfi, rfi, false, appending, okayToPage ); break; } case TextParagraph.CENTERED: { int virtual_width = width - li - ri; StringBuffer sb = new StringBuffer(); StringTokenizer st = new StringTokenizer( message, "\n", true ); while( st.hasMoreTokens() ) { String this_line = st.nextToken(); if( this_line.equals( "\n" ) ) sb.append( '\n' ); else { int this_line_length = stringLength( this_line ); // try to center this line int i = ((virtual_width - this_line_length) / 2) - 1; // only center it if we can if( i > 0 ) sb.append( spaces( i ) ); sb.append( this_line ); } } paragraph( sb.toString(), li, ri, lfi, rfi, false, appending, okayToPage ); break; } default: paragraph( message, li, ri, lfi, rfi, true, appending, okayToPage ); } if( !isPaging() ) { terminal.reset( output ); output.write( CRLF ); } } } protected void sendColumnParagraph( ColumnParagraph tp, boolean appending, boolean okayToPage ) throws IOException { // first, calculate the number of columns to display on this // width screen. int numCols = width / (tp.maxEntryWidth + tp.spaceBetween); int indent = (width - (numCols * tp.maxEntryWidth)) / 2; int pos = 0; StringBuffer sb = new StringBuffer(); for( Enumeration e = tp.elements(); e.hasMoreElements(); ) { String ts = (String) e.nextElement(); int tsl = stringLength( ts ); int trailer = tp.spaceBetween; if( tsl <= tp.maxEntryWidth ) { sb.append( ts ); trailer += tp.maxEntryWidth - tsl; } else sb.append( ts.substring( 0, tp.maxEntryWidth ) ); pos = (++pos) % numCols; if( pos == 0 ) sb.append( '\n' ); else sb.append( spaces( trailer ) ); } paragraph( sb.toString(), indent, 0, 0, 0, false, appending, okayToPage ); if( !isPaging() ) { terminal.reset( output ); output.write( CRLF ); } } protected void sendTableParagraph( TableParagraph tp, boolean appending, boolean okayToPage ) throws IOException { // first, calculate the number of columns to display on this // width screen. // start at 1, there's a | on the left side int currentWidth = 1; int lastColumn = -1; // add lastColumn to allow for the | seperators while( currentWidth + lastColumn < width ) { lastColumn++; if( lastColumn >= tp.columns.length ) break; currentWidth += tp.columns[ lastColumn ].getWidth(); } if( lastColumn < tp.columns.length ) currentWidth -= tp.columns[ lastColumn ].getWidth(); // add lastColumn to allow for the | seperators currentWidth += lastColumn; lastColumn--; if( lastColumn < 0 ) { // not enough room to display anything return; } // now generate the text itself StringBuffer sb = new StringBuffer(); StringBuffer secondRow = new StringBuffer( "|" ); // add the top heading rows for( int i = 0; i <= lastColumn; i++ ) { String cheading = tp.columns[ i ].getHeading(); int cwidth = tp.columns[ i ].getWidth(); int chl = stringLength( cheading ); secondRow.append( dashes( cwidth ) ); secondRow.append( '|' ); sb.append( ' ' ); if( cwidth > chl ) { sb.append( ' ' ); sb.append( cheading ); if( i != lastColumn ) sb.append( spaces( cwidth - chl - 1 ) ); } else if( cwidth == chl ) { sb.append( cheading ); } else { sb.append( cheading.substring( 0, cwidth ) ); } } sb.append( '\n' ); sb.append( secondRow.toString() ); secondRow = null; // now add the rows for the data for( Enumeration e = tp.elements(); e.hasMoreElements(); ) { String[] rd = (String[]) e.nextElement(); sb.append( '\n' ); sb.append( ' ' ); for( int i = 0; i <= lastColumn; i++ ) { if( rd[i] == null ) { sb.append( spaces( tp.columns[i].getWidth() ) ); } else { int rdl = stringLength( rd[i] ); int cwidth = tp.columns[i].getWidth(); if( cwidth >= rdl ) { sb.append( rd[i] ); sb.append( spaces( cwidth - rdl ) ); } else sb.append( rd[i].substring( 0, cwidth ) ); } sb.append( ' ' ); } } sb.append( '\n' ); // now add the footer String tm = tp.getFooter(); if( tm != null ) { int tl = stringLength( tm ); if( tl > 0 ) { if( tl+10 < currentWidth ) { // right justify it sb.append( '|' ); sb.append( dashes( currentWidth - tl - 7 ) ); sb.append( ' ' ); sb.append( tm ); sb.append( " ---|" ); } else sb.append( dashes( currentWidth ) ); } else sb.append( dashes( currentWidth ) ); } else sb.append( dashes( currentWidth ) ); paragraph( sb.toString(), 0, 0, 0, 0, false, appending, okayToPage ); if( !isPaging() ) { terminal.reset( output ); output.write( CRLF ); } } protected void sendHeadingParagraph( String message, int alignment, boolean appending, boolean okayToPage ) throws IOException { StringBuffer sb = new StringBuffer(); int ml = stringLength( message ); if( ml + 2 >= width ) { sb.append( dashes( 3 ) ); sb.append( " " ); sb.append( message ); sb.append( " " ); sb.append( dashes( 3 ) ); } else { if( alignment == HeadingParagraph.LEFT ) { sb.append( dashes( 3 ) ); sb.append( " " ); sb.append( message ); sb.append( " " ); sb.append( dashes( width - (ml + 7) ) ); } else if( alignment == HeadingParagraph.RIGHT ) { sb.append( dashes( width - (ml + 7) ) ); sb.append( " " ); sb.append( message ); sb.append( " " ); sb.append( dashes( 3 ) ); } else { int a = (width - (ml + 4)) / 2; sb.append( dashes( a ) ); sb.append( " " ); sb.append( message ); sb.append( " " ); sb.append( dashes( (width - (ml + 4)) - a ) ); } } paragraph( sb.toString(), 0, 0, 0, 0, false, appending, okayToPage ); if( !isPaging() ) { terminal.reset( output ); output.write( CRLF ); } } public final void send( Paragraph para ) { send( para, true ); } public void send( Paragraph para, boolean okayToPage ) { try { okayToPage = okayToPage && canPageEver; if( para instanceof TextParagraph ) { TextParagraph tp = (TextParagraph) para; sendTextParagraph( tp.getLeftMargin(), tp.getRightMargin(), tp.getLeftFirstMargin(), tp.getRightFirstMargin(), tp.getText(), tp.getAlignment(), false, okayToPage ); } else if( para instanceof BlankLineParagraph ) { blankLine(); } else if( para instanceof HeadingParagraph ) { HeadingParagraph hp = (HeadingParagraph) para; sendHeadingParagraph( hp.getText(), hp.getAlignment(), false, okayToPage ); } else if( para instanceof LineParagraph ) { sendLine(); } else if( para instanceof TableParagraph ) { sendTableParagraph( ((TableParagraph)para), false, okayToPage ); } else if( para instanceof ColumnParagraph ) { sendColumnParagraph( ((ColumnParagraph)para), false, okayToPage ); } else if( para instanceof MultiParagraph ) { // FUTURE: Make it so it only calls flush() once, and // so that it pages things properly. atm if // a multiparagraph started with, for instance, // 20 line paragraphs and then some text, it // wouldn't page the lines, even though it might // page the text. Some sort of clever paragraph // routine might be able to do something like this, // but I hardly think anyone's going to bother. I'm // sure not going to. // // Another option is to change paragraph() so that it // doesn't output any data, just returns the data // to be output - or some other 'middle' routine between // the usual paragraph() call that invokes the pager // for internal private use. for( Enumeration e = ((MultiParagraph)para).getParagraphs(); e.hasMoreElements(); ) { Object o = e.nextElement(); // recheck, we now have to call some of these routines // with an 'appending' flag if( o instanceof TextParagraph ) { TextParagraph tp = (TextParagraph) o; sendTextParagraph( tp.getLeftMargin(), tp.getRightMargin(), tp.getLeftFirstMargin(), tp.getRightFirstMargin(), tp.getText(), tp.getAlignment(), true, okayToPage ); } else if( o instanceof HeadingParagraph ) { HeadingParagraph hp = (HeadingParagraph) o; sendHeadingParagraph( hp.getText(), hp.getAlignment(), true, okayToPage ); } else if( o instanceof BlankLineParagraph ) { if( okayToPage && pageBuffer != null ) pageBuffer.addElement( new String( CRLF ) ); else blankLine(); } else if( o instanceof LineParagraph ) { if( okayToPage && pageBuffer != null ) pageBuffer.addElement( new String( dashes( width ) + CRLF ) ); else sendLine(); } else if( para instanceof ColumnParagraph ) sendColumnParagraph( ((ColumnParagraph)o), true, okayToPage ); else if( para instanceof TableParagraph ) sendTableParagraph( ((TableParagraph)o), true, okayToPage ); else send( (Paragraph)o, okayToPage ); } } } catch( IOException e ) { //close(); //throw new NetworkException( e ); } } public void blankLines( int c ) { try { while( c-- > 0 ) output.write( CRLF ); } catch( IOException e ) { //close(); //throw new NetworkException( e ); } } public void blankLine() { try { output.write( CRLF ); } catch( IOException e ) { //close(); //throw new NetworkException( e ); } } /** * You never know what they _might_ have available on their terminal... * *grin* */ public void sendSubliminal( String message, int duration, int frequency ) { try { if( teloptSubliminal.enabledHim() ) { output.write( IAC ); output.write( SB ); output.write( TelnetOption.TELOPT_SUBLIMINAL ); write16Int( duration ); write16Int( frequency ); output.write( message ); output.write( IAC ); output.write( SE ); output.flush(); } } catch( IOException e ) { //close(); //throw new NetworkException( e ); } } /** * A not-so-trivial routine that does word-wrap'ing WITH * colour codes, and outputs the result through the * pager (if required) down the users terminal. * <p> * <i>(Funny... this routine was a lot harder to write * in C++) (1998: Funny, in C++ it was also a lot faster)</i> * <p> * Anything put in preLine is additional to the left * indent - so to guarantee a left indent of 4, ensure * you subtract the length of preLine * <p> * Setting prelines that are wider than the * terminal has an undefined effect on the output. * * @param message The text to format * @param li <b>L</b>eftedge <b>I</b>ndent * @param ri <b>R</b>ightedge <b>I</b>ndent * @param lfi <b>L</b>eftedge <b>F</b>irstline <b>I</b>ndent (in addition to the leftedge) * @param rfi <b>R</b>ightedge <b>F</b>irstline <b>I</b>ndent (in addition to the rightedge) * @param preLine A string constant to put at the start of every line */ public void paragraph( String message, int li, int ri, int lfi, int rfi, boolean stripLeadingSpaces, boolean appending, boolean okayToPage ) throws IOException { StringBuffer gen = new StringBuffer(); if( !wordwrap ) { // without word wrap, we can't page effectively either, // so we don't even bother. It makes our job a lot easier, // all we need to do is substitute CRLF for the CR's the // program uses internally, in order to conform to the // telnet protocol. StringTokenizer lines = new StringTokenizer( message, "\n", true ); while( lines.hasMoreTokens() ) { String str = lines.nextToken(); if( str.equals( "\n" ) ) gen.append( CRLF ); else { gen.append( spaces( li + lfi ) ); // so it only gets used once lfi = 0; // here we insert our check for colour codes - // if the line has a ^ in it, there is something // we have to filter out :) // // this algorithm is modified from the more // complex one below, that also generates word // lengths (for the wordwrap). please copy the // other one, if you have to. int colourIndex = str.indexOf( '^' ); int start = 0; while( colourIndex != -1 && (colourIndex+1) < str.length() ) { gen.append( str.substring( start, colourIndex ) ); char c = str.charAt( colourIndex + 1 ); if( c == '^' ) { // its been doubled to display it - thats cool gen.append( c ); start = colourIndex+2; } else { // lets just skip one character start = colourIndex+2; gen.append( terminal.stringForCode( c ) ); } colourIndex = str.indexOf( '^', start ); } gen.append( str.substring( start ) ); } } output.write( gen.toString() ); return; } Vector generatedLines = new Vector( 5, 25 ); okayToPage = okayToPage && canPageEver; int x=0; // x position on the virtual screen boolean firstLine = true; boolean startOfLine = true; StringTokenizer lines = new StringTokenizer( message, "\n", true ); while( lines.hasMoreTokens() ) { String str = lines.nextToken(); // internally, the program uses *only* // a single \n to represent a new line. // The telnet protocol demands, however, // that \n\r be used to represent this. // What we do here is catch \n's and turn // them into \n\r's. if( str.equals( "\n" ) ) { gen.append( CRLF ); generatedLines.addElement( gen.toString() ); gen = new StringBuffer(); x = 0; firstLine = false; startOfLine = true; } else { StringTokenizer st = new StringTokenizer( str, " ", true ); if( x == 0 ) { gen.append( spaces( li + lfi ) ); x += li + lfi; startOfLine = true; } while( st.hasMoreTokens() ) { // the actual word to insert into the stream String thisWord = st.nextToken(); // just a shortcut in case we're just dealing with // a space if( thisWord.equals( " " ) ) { if( !stripLeadingSpaces || !startOfLine ) { if( !startOfLine ) { x++; // start a new line if we go too far if( x >= width ) { gen.append( CRLF ); generatedLines.addElement( gen.toString() ); gen = new StringBuffer( spaces( li ) ); x = li; firstLine = false; startOfLine = true; } else gen.append( " " ); } else gen.append( " " ); } continue; } // the word that will appear - use it for // getting lengths and stuff - colour // codes are filtered out int wordLength; // this is the colour matching scope { int wl = 0; // tmp wordlength // here we insert our check for colour codes - // if the word has a ^ in it, there is something // we have to filter out :) int colourIndex = thisWord.indexOf( '^' ); int start = 0; StringBuffer fullWord = new StringBuffer(); while( colourIndex != -1 && (colourIndex+1) < thisWord.length() ) { fullWord.append( thisWord.substring( start, colourIndex ) ); wl += colourIndex - start; char c = thisWord.charAt( colourIndex + 1 ); if( c == '^' ) { // its been doubled to display it - thats cool wl++; fullWord.append( c ); start = colourIndex+2; } else { // lets just skip one character start = colourIndex+2; fullWord.append( terminal.stringForCode( c ) ); } colourIndex = thisWord.indexOf( '^', start ); } wl += thisWord.length() - start; fullWord.append( thisWord.substring( start ) ); //if( wl != thisWord.length() ) //System.out.println( "word '" + thisWord + "' converted to '" + fullWord.toString() + "', length " + wl ); thisWord = fullWord.toString(); wordLength = wl; } // incidently, EW had a feature here // which enabled you to say how big a // word can be before its split at the // end of a line. This isn't implemented // here yet, I'm just noting down the fact // that its an idea. if( firstLine ) { // first line // if this word is going to wrap and the word // isn't longer than a full line by itself if( ( (wordLength + x) > (width - (ri+rfi)) ) && (wordLength < (width-(ri+li)) ) ) { gen.append( CRLF ); generatedLines.addElement( gen.toString() ); gen = new StringBuffer( spaces( li ) ); x = li; firstLine = false; startOfLine = true; } } else { // if this word is going to wrap and the word // isn't longer than a full line by itself if( ( (wordLength + x) > (width - ri) ) && (wordLength < (width-ri-li) ) ) { gen.append( CRLF ); generatedLines.addElement( gen.toString() ); gen = new StringBuffer( spaces( li ) ); x = li; startOfLine = true; } } // this if doesn't add the word if its a // space, we're at the start of a line, // and we're stripping leading spaces. if( !stripLeadingSpaces || !startOfLine ) { gen.append( thisWord ); x += wordLength; startOfLine = false; } else { if( !thisWord.equals( " " ) ) { gen.append( thisWord ); x += wordLength; startOfLine = false; } // we don't reset the startOfLine if // we're stripping and its just a space, // in order to strip more than one space // at the start of the line... } } } } if( gen.length() > 0 ) generatedLines.addElement( gen.toString() ); // gen.toString() now contains the // buffer to be displayed // generatedLines contain the amount of lines // ready to be displayed int numberOfLines = generatedLines.size(); if( !okayToPage || ((!appending) && numberOfLines + PAGER_THRESHOLD <= height) ) { for( int i = 0; i < numberOfLines; i++ ) output.write( (String) generatedLines.elementAt( i ) ); } else { // invoke the pager // if we're already paging through something, simply // overwrite it. (Don't append to a page buffer, // unless explicitly told to) if( appending && pageBuffer != null ) { for( int i = 0; i < numberOfLines; i++ ) pageBuffer.addElement( generatedLines.elementAt( i ) ); } else { pageBuffer = generatedLines; topOfScreen = 0; drawNextPage(); } } } public boolean canPage() { return( canPageEver ); } public void setCanPage( boolean b ) { canPageEver = b; } public boolean isPaging() { return( pageBuffer != null ); } public void quitPager() { pageBuffer = null; topOfScreen = 0; } public void drawNextPage() { try { if( pageBuffer == null || topOfScreen >= pageBuffer.size() ) { sendError( PAGER_LACKS_PAGES ); quitPager(); return; } int totalSize = pageBuffer.size(); int newTop = topOfScreen + height - PAGER_VERTICAL_SPACE; if( newTop > totalSize ) newTop = totalSize; for( int i = topOfScreen; i < newTop; i++ ) output.write( (String) pageBuffer.elementAt( i ) ); output.write( CRLF ); if( newTop >= totalSize ) { // we're done, that was the last page quitPager(); } else { send( "<Pager: lines " + topOfScreen + " to " + (newTop-1) + ", from a total " + totalSize + ": (^hn^-)ext page or (^hq^-)uit>" ); topOfScreen = newTop; } } catch( IOException e ) { //close(); //throw new NetworkException( e ); } } /* This is some old code which generated the un-touched word from a word with colourcodes in it - it wasn't useful, so its been removed // this is the colour matching scope { StringBuffer appearBuffer = new StringBuffer(); // here we insert our check for colour codes - // if the word has a ^ in it, there is something // we have to filter out :) int colourIndex = thisWord.indexOf( '^' ); int start = 0; while( colourIndex != -1 && colourIndex < thisWord.length() ) { sb.append( thisWord.substring( start, colourIndex ) ); char c = thisWord.charAt( colourIndex + 1 ); if( c == '^' ) { // its been doubled to display it - thats cool sb.append( c ); start = colourIndex+2; } else { // lets just skip one character start = colourIndex+2; } colourIndex = thisWord.indexOf( '^', start ); } else sb.append( thisWord.substring( start ) ); send( substitute( appearWord = appearBuffer.toString(); } */ // 70 spaces - requesting more than this will // slow the routine down private static final String SPACES=" "; private static final int SPACES_LENGTH = SPACES.length(); /** * Returns a string of the specified number of spaces * <p> * This routine has been optimised for up to 70 spaces, * requesting more than this will slow it down. */ public static String spaces( int n ) { if( n > SPACES_LENGTH ) { StringBuffer sb = new StringBuffer(); while( n > SPACES_LENGTH ) { sb.append( SPACES ); n -= SPACES_LENGTH; } sb.append( SPACES.substring( SPACES_LENGTH - n ) ); return( sb.toString() ); } else return( SPACES.substring( SPACES_LENGTH - n ) ); } // 70 dashes - requesting more than this will // slow the routine down private static final String DASHES="----------------------------------------------------------------------"; private static final int DASHES_LENGTH = DASHES.length(); /** * Returns a string of the specified number of dashes * <p> * This routine has been optimised for up to 70 spaces, * requesting more than this will slow it down. */ public static String dashes( int n ) { if( n > DASHES_LENGTH ) { StringBuffer sb = new StringBuffer(); while( n > DASHES_LENGTH ) { sb.append( DASHES ); n -= DASHES_LENGTH; } sb.append( DASHES.substring( DASHES_LENGTH - n ) ); return( sb.toString() ); } else return( DASHES.substring( DASHES_LENGTH - n ) ); } /** * Calculates the length of a string, discarding colourcodes. * * [ META: Should probably move to a different location.] */ public static final int stringLength( String s ) { int start = 0; int subtraction = 0; int match; int length = s.length(); while( true ) { if( start < length ) { match = s.indexOf( '^', start ); if( match != -1 ) { subtraction+=2; try { if( s.charAt( match+1 ) == '^' ) subtraction--; } catch( StringIndexOutOfBoundsException e ) { break; } start = match+2; continue; } } break; } return( length - subtraction ); } /** * Will is Us, Do is them. */ static class TelnetOption { /** * The number of the code that we are */ public int teloptCode; // telnet options public static final int TELOPT_BINARY=0; public static final int TELOPT_ECHO=1; public static final int TELOPT_SGA=3; public static final int TELOPT_TTYPE=24; public static final int TELOPT_EOR=25; public static final int TELOPT_NAWS=31; public static final int TELOPT_LINEMODE=34; public static final int TELOPT_SUBLIMINAL=257; // variables required by the Q method - RFC 1143 public int us; public int usq; public int him; public int himq; /** * True if we're allowed to enable this option in that * way (if we can Do this option or if we can support it) */ public boolean canDo; public boolean canWill; public static final int NO = 0; public static final int YES = 1; public static final int WANTNO = 2; public static final int WANTYES = 3; public static final int EMPTY = 4; public static final int OPPOSITE = 5; public static final int NONE = 6; public TelnetOption( int code, boolean supportHim, boolean supportUs ) { teloptCode = code; us = NO; him = NO; usq = EMPTY; himq = EMPTY; canDo = supportHim; canWill = supportUs; } public final boolean enabledUs() { return( us == YES ); } public final boolean enabledHim() { return( him == YES ); } /** * Attempts to initiate a don't message for this option * to the output stream */ public void sendDont( Writer out ) throws IOException { //Log.debug( this, "sendDon't called" ); switch( him ) { case NO: Log.debug( this, "ERROR: Already disabled" ); break; case YES: him = WANTNO; out.write( TelnetIC.IAC ); out.write( TelnetIC.DONT ); out.write( teloptCode ); out.flush(); break; case WANTNO: switch( himq ) { case EMPTY: Log.debug( this, "ERROR: Already negotiating for disable" ); break; case OPPOSITE: himq = EMPTY; break; } break; case WANTYES: switch( himq ) { case EMPTY: Log.debug( this, "ERROR: Cannot initiate new request in the middle of negotiation" ); break; case OPPOSITE: Log.debug( this, "ERROR: Already queued a disable request" ); break; } break; } } /** * Attempts to initiate a do message for this option * to the output stream */ public void sendDo( Writer out ) throws IOException { if( canDo ) { //Log.debug( this, "sendDo called" ); switch( him ) { case NO: him = WANTYES; out.write( TelnetIC.IAC ); out.write( TelnetIC.DO ); out.write( teloptCode ); out.flush(); break; case YES: Log.debug( this, "ERROR: Already disabled" ); break; case WANTNO: switch( himq ) { case EMPTY: Log.debug( this, "ERROR: Cannot initiate new request in the middle of negotiation" ); break; case OPPOSITE: Log.debug( this, "ERROR: Already queued a enable request" ); break; } break; case WANTYES: switch( himq ) { case EMPTY: Log.debug( this, "ERROR: Already negotiating for enable" ); break; case OPPOSITE: himq = EMPTY; break; } break; } } else Log.debug( this, "ERROR: Attempting to sendDo when can false" ); } /** * Attempts to initiate a don't message for this option * to the output stream */ public void sendWont( Writer out ) throws IOException { //Log.debug( this, "sendWon't called" ); switch( us ) { case NO: Log.debug( this, "ERROR: Already disabled" ); break; case YES: us = WANTNO; out.write( TelnetIC.IAC ); out.write( TelnetIC.WONT ); out.write( teloptCode ); out.flush(); break; case WANTNO: switch( usq ) { case EMPTY: Log.debug( this, "ERROR: Already negotiating for disable" ); break; case OPPOSITE: usq = EMPTY; break; } break; case WANTYES: switch( usq ) { case EMPTY: Log.debug( this, "ERROR: Cannot initiate new request in the middle of negotiation" ); break; case OPPOSITE: Log.debug( this, "ERROR: Already queued a disable request" ); break; } break; } } /** * Attempts to initiate a do message for this option * to the output stream */ public void sendWill( Writer out ) throws IOException { //Log.debug( this, "sendWill called" ); if( canWill ) { switch( us ) { case NO: us = WANTYES; out.write( TelnetIC.IAC ); out.write( TelnetIC.WILL ); out.write( teloptCode ); out.flush(); break; case YES: Log.debug( this, "ERROR: Already enabled" ); break; case WANTNO: switch( usq ) { case EMPTY: Log.debug( this, "ERROR: Cannot initiate new request in the middle of negotiation" ); break; case OPPOSITE: Log.debug( this, "ERROR: Already queued an enable request" ); break; } break; case WANTYES: switch( usq ) { case EMPTY: Log.debug( this, "ERROR: Already negotiating for enable" ); break; case OPPOSITE: usq = EMPTY; break; } break; } } else Log.debug( this, "ERROR: Attempting to sendWill when can false" ); } public void rcptWont( Writer out ) throws IOException { //Log.debug( this, "rcptWon't called" ); switch( him ) { case NO: // ignore break; case YES: him = NO; out.write( TelnetIC.IAC ); out.write( TelnetIC.DONT ); out.write( teloptCode ); out.flush(); break; case WANTNO: switch( himq ) { case EMPTY: him = NO; break; case OPPOSITE: him = WANTYES; himq = NONE; out.write( TelnetIC.IAC ); out.write( TelnetIC.DO ); out.write( teloptCode ); out.flush(); break; } break; case WANTYES: switch( himq ) { case EMPTY: him = NO; break; case OPPOSITE: him = NO; himq = NONE; break; } break; } } public void rcptWill( Writer out ) throws IOException { //Log.debug( this, "rcptWill called" ); switch( him ) { case NO: if( canDo ) { him = YES; out.write( TelnetIC.IAC ); out.write( TelnetIC.DO ); out.write( teloptCode ); out.flush(); } else { out.write( TelnetIC.IAC ); out.write( TelnetIC.DONT ); out.write( teloptCode ); out.flush(); } break; case YES: // ignore break; case WANTNO: Log.debug( this, "ERROR: Don't answered by Will." ); switch( himq ) { case EMPTY: him = NO; break; case OPPOSITE: him = YES; himq = EMPTY; break; } break; case WANTYES: switch( himq ) { case EMPTY: him = YES; break; case OPPOSITE: him = WANTNO; himq = EMPTY; out.write( TelnetIC.IAC ); out.write( TelnetIC.DONT ); out.write( teloptCode ); out.flush(); break; } break; } } public void rcptDont( Writer out ) throws IOException { //Log.debug( this, "rcptDon't called" ); switch( us ) { case NO: // ignore break; case YES: us = NO; out.write( TelnetIC.IAC ); out.write( TelnetIC.WONT ); out.write( teloptCode ); out.flush(); break; case WANTNO: switch( usq ) { case EMPTY: us = NO; break; case OPPOSITE: us = WANTYES; usq = NONE; out.write( TelnetIC.IAC ); out.write( TelnetIC.WILL ); out.write( teloptCode ); out.flush(); break; } break; case WANTYES: switch( usq ) { case EMPTY: us = NO; break; case OPPOSITE: us = NO; usq = NONE; break; } break; } } public void rcptDo( Writer out ) throws IOException { //Log.debug( this, "rcptDo called" ); switch( us ) { case NO: if( canWill ) { us = YES; out.write( TelnetIC.IAC ); out.write( TelnetIC.WILL ); out.write( teloptCode ); out.flush(); } else { out.write( TelnetIC.IAC ); out.write( TelnetIC.WONT ); out.write( teloptCode ); out.flush(); } break; case YES: // ignore break; case WANTNO: Log.debug( this, "ERROR: Won't answered by Do." ); switch( usq ) { case EMPTY: us = NO; break; case OPPOSITE: us = YES; usq = EMPTY; break; } break; case WANTYES: switch( usq ) { case EMPTY: us = YES; break; case OPPOSITE: us = WANTNO; usq = EMPTY; out.write( TelnetIC.IAC ); out.write( TelnetIC.WONT ); out.write( teloptCode ); out.flush(); break; } break; } } /* public String toString() { return( "[TelnetOption " + teloptCode + "]: } */ void negotiation( TelnetIC tc ) throws IOException { } } final static class NAWSTelnetOption extends TelnetOption { public NAWSTelnetOption( int code, boolean supportHim, boolean supportUs ) { super( code, supportHim, supportUs ); } /** * This is the routine to handle the subnegotiation * of the telnet window size option */ void negotiation( TelnetIC tc ) throws IOException { try { tc.width = tc.read16Int()-1; tc.height = tc.read16Int(); int a = tc.width; int b = tc.height; if( tc.width < TelnetIC.SMALLEST_BELIEVABLE_WIDTH || tc.width > TelnetIC.LARGEST_BELIEVABLE_WIDTH ) tc.width = TelnetIC.DEFAULT_WIDTH; if( tc.height < TelnetIC.SMALLEST_BELIEVABLE_HEIGHT || tc.height > TelnetIC.LARGEST_BELIEVABLE_HEIGHT ) tc.height = TelnetIC.DEFAULT_HEIGHT; if( a != tc.width || b != tc.height ) Log.debug( this, "Didn't accepted Negotiated window size of : " + a + "x" + b + ", instead using: " + tc.width + "x" + tc.height ); //else //tc.send( "Screen size of " + tc.width + "x" + tc.height + " detected" ); int i; i = tc.readInt(); if( i == TelnetIC.IAC ) { switch( tc.handleIAC() ) { case TelnetIC.SE: break; default: Log.debug( this, "ERROR: Didn't receive expected SE" ); break; } } else Log.debug( this, "ERROR: Didn't receive expected SE" ); } catch( IACRecievedException e ) { Log.debug( this, e.toString() ); } } } final static class TerminalTypeTelnetOption extends TelnetOption { static final int IS=0; // used in Terminal-Type negotiations static final int SEND=1; String last=""; TelnetIC telnet; public TerminalTypeTelnetOption( TelnetIC tc, int code, boolean supportHim, boolean supportUs ) { super( code, supportHim, supportUs ); telnet = tc; } /** * This is the routine to handle the subnegotiation * of the telnet terminal type option */ void negotiation( TelnetIC tc ) throws IOException { int i = tc.readInt(); if( i == IS ) { // receiving a terminal type String s = tc.readIACSETerminatedString(); if( !telnet.setDetectedTerminal( s ) ) { //Log.debug( this, "failed terminal match: " + s ); if( !last.equals( s ) ) { last = s; requestAnotherTerminal( tc.output ); //Log.debug( this, "getting another terminal..." ); } } //else //Log.debug( this, "made terminal match: " + telnet.getTerminal().getName() ); } } public void rcptWill( Writer out ) throws IOException { super.rcptWill( out ); if( enabledHim() ) { requestAnotherTerminal( out ); } } void requestAnotherTerminal( Writer out ) throws IOException { out.write( TelnetIC.IAC ); out.write( TelnetIC.SB ); out.write( TELOPT_TTYPE ); out.write( SEND ); out.write( TelnetIC.IAC ); out.write( TelnetIC.SE ); out.flush(); } } /** * For echos, rather than implement some sort of complicated * queueing system for options, I choose to simply always * send do and won't requests, and to ignore the replies. This * is what EW2 does with all options. * * I understand that this isn't a particularly wonderful implementation, * however, echo gets turned on and off for passwords, and an auto-connect * client (like tinyfugue) can send the password before it responds to the * echo requests that it recieves. We would, conceivably, only need a * queue of length 2 in order to implement this properly, but its a * step that I'm not willing to make at the moment, for fear of screwing * it up. I daresay that I only managed to get the options working at * all with several days effort and the help of the Q-method RFC. (I * forget the number for it itself, its one of the ones listed at the * top of the file. - subtle */ final static class EchoTelnetOption extends TelnetOption { public EchoTelnetOption( int code, boolean supportHim, boolean supportUs ) { super( code, supportHim, supportUs ); } /** * Attempts to initiate a don't message for this option * to the output stream */ public final void sendDont( Writer out ) throws IOException { //Log.debug( this, "sendDon't called" ); out.write( TelnetIC.IAC ); out.write( TelnetIC.DONT ); out.write( teloptCode ); out.flush(); } /** * Attempts to initiate a do message for this option * to the output stream */ public final void sendDo( Writer out ) throws IOException { out.write( TelnetIC.IAC ); out.write( TelnetIC.DO ); out.write( teloptCode ); out.flush(); } /** * Attempts to initiate a don't message for this option * to the output stream */ public final void sendWont( Writer out ) throws IOException { out.write( TelnetIC.IAC ); out.write( TelnetIC.WONT ); out.write( teloptCode ); out.flush(); } /** * Attempts to initiate a do message for this option * to the output stream */ public final void sendWill( Writer out ) throws IOException { out.write( TelnetIC.IAC ); out.write( TelnetIC.WILL ); out.write( teloptCode ); out.flush(); } public void rcptWont( Writer out ) { // ignore } public void rcptWill( Writer out ) { // ignore } public void rcptDont( Writer out ) { // ignore } public void rcptDo( Writer out ) { // ignore } } final static class IACRecievedException extends Exception { int following; public IACRecievedException( int codeAfter ) { super( "IAC recieved unexpectedly" ); following = codeAfter; } } }