From: Damien Kick
Subject: Stumbling towards implementing telnet
Date: 
Message-ID: <Adubh.4927$ql2.4281@newsread3.news.pas.earthlink.net>
;;; So I've started trying to think of how to implement a
;;; telnet-stream with simple streams.  But I got stuck (see comments
;;; below) because, IMHO, the simple streams API is <pause/> not so
;;; simple.  So I've started with this quasi-stream; it kinda acts
;;; like a stream but does not work with read/write of byte/char.
;;; Instead, to avoid symbol conflicts, I use the similar names
;;; send/recv of byte/char.  By this point, I'm able to successfully
;;; get a Solaris telnet server to ignore me <grin type="weak"/> and a
;;; telnet server running on a Tandem to at least send me the login
;;; prompt, after a long pause after a short burst of options
;;; negotiations, but then not respond to my attempt to login.  I
;;; think my first attempt at something basic, i.e telling the server
;;; that I won't do anything requested of me, is too anti-social.  I'm
;;; also probably doing something wrong the the go-ahead.  <grumble/>
;;; Of course, Erik Naggum was right <http://tinyurl.com/voks9>,
;;; telnet is not something one whips up in an hour or two.

;;; So I think that I need to figure out what I'm doing wrong with my
;;; attempt at negotiating telnet options.  Can somebody at least help
;;; me with translating this broken beginning to a broken version
;;; based on simple streams?

;;; For what it's worth:
;;;
;;; PG-USER> (lisp-implementation-type)
;;; "International Allegro CL Professional Edition"
;;; PG-USER> (lisp-implementation-version)
;;; "8.0 [Mac OS X] (Nov 11, 2006 22:53)"
;;; PG-USER>

(in-package :net.earthlink.dkixk.telnet-stream)

(defconstant +iac/byte-code+ 255
   "Integer value for telnet IAC (is a command), used to indicate the
beginning of a sequence used for negotiating telnet options.")

(defconstant +will/byte-code+ 251)
(defconstant +will-not/byte-code+ 252)
(defconstant +do/byte-code+ 253)
(defconstant +do-not/byte-code+ 254)

;; The following are the telnet options I'm finding either the Solaris
;; telnet server or the Tandem telnet server is trying to negotiate
;; with me.
(defconstant +echo/byte-code+ 1)
(defconstant +suppress-go-ahead/byte-code+ 3)
(defconstant +terminal-type/byte-code+ 24)
(defconstant +window-size/byte-code+ 31)
(defconstant +xdisploc/byte-code+ 35)
(defconstant +newenv/byte-code+ 39)
(defconstant +environ/byte-code+ 36)

;; It would seem to me to I would not want telnet-stream to use
;; encapsulation.  Encapsulating streams seems to me to what sense
;; when I would, for example, want to be able to wrap a telnet-stream
;; around something other than a tcp socket, e.g. a file-stream.
;; However, telnet only makes sense over TCP, so it would seem that
;; this should be "hardcoded" into the class hierarchy.  Does that
;; make sense?
(defclass telnet-quasi-stream ()
     ((tcp-stream :reader tcp-stream)))

(defmethod initialize-instance :after
     ((object telnet-quasi-stream)
      &key remote-host (remote-port (lookup-port :telnet :tcp)))
   (with-slots (tcp-stream) object
     (setq tcp-stream (make-socket :remote-host remote-host
                                   :remote-port remote-port))
     ;; I think that the following two forms are redundant but they
     ;; also don't seem to make a difference in the behavior of either
     ;; telnet server.
     (send-do +suppress-go-ahead/byte-code+ object)
     (send-will +suppress-go-ahead/byte-code+ object)
     (send-do-not +echo/byte-code+ object)))

(defun telnet (remote-host &optional
                            (remote-port (lookup-port :telnet :tcp)))
   (make-instance 'telnet-quasi-stream
                  :remote-host remote-host :remote-port remote-port))

(defmethod close ((stream telnet-quasi-stream) &key abort)
   (with-slots (tcp-stream tcp-stream) stream
     (when tcp-stream
       (prog1 (close tcp-stream :abort abort)
         (setq tcp-stream nil)))))

(defun send-char (char telnet-stream)
   (assert (swank::ascii-char-p char))
   (write-char char (tcp-stream telnet-stream)))

;; A device-write for a simple streams version would want to escape a
;; byte of 255 for normal data.  However, at the device level, I can
;; not figure out how to have something like a send-option (see
;; below) indicate to the device level that its particular 255 byte
;; should not be escpaed.  I would want a simple write-byte to be the
;; function for sending normal data and another function specific to
;; sending commands to be used <pause/> for sending commands.
;; Wouldn't I need access to the strategy level for something like
;; this?  Such a send-option could not simply write to the underlying
;; TCP stream because the command would not be sequenced properly with
;; the data already buffered by previous write-byte.  send-option
;; could not call device-write directly.
(defun send-byte (byte telnet-stream)
   #-(and) (assert (transmit-binary telnet-stream))
   (when (= byte +iac/byte-code+)
     (write-byte +iac/byte-code+ (tcp-stream telnet-stream)))
   (write-byte byte (tcp-stream telnet-stream)))

(defun recv-char (stream &optional eof-error-p eof-value)
   (recv-octet #'code-char stream eof-error-p eof-value))

(defun recv-byte (stream &optional eof-error-p eof-value)
   (recv-octet #'identity stream eof-error-p eof-value))

;; This is the general shape of what might be device-read for a simple
;; streams version.  However, I'm not sure what one should use for
;; read-byte.  I'm aware that read-octet can be used at the device
;; level.  The analogy with this quasi-stream would be to use
;; read-sequence or read-vector, I suppose.  However, this first
;; simple version wants to avoid complications for the case when one
;; finds the beginning of a command at the end of a sequence.  For
;; example, read-octet returns n octets, and (aref buffer (1- n)) is
;; the IAC byte.
(defun recv-octet (fn telnet-stream &optional eof-error-p eof-value)
   (with-accessors ((tcp-stream tcp-stream)) telnet-stream
     (flet ((%read-byte () (read-byte tcp-stream eof-error-p eof-value)))
       (loop for byte = (%read-byte) while byte
             if (= byte +iac/byte-code+) do
             (let ((command-byte-code (%read-byte)))
               (cond ((= command-byte-code +iac/byte-code+)
                      ;; Not really a command-code but rather an
                      ;; escapded data value 255.
                      (return (funcall fn command-byte-code)))
                     ((= command-byte-code +will/byte-code+)
                      (let ((option-byte-code (%read-byte)))
                        ;; At the moment, we're only ready to "deal"
                        ;; with somebody accepting our request to
                        ;; suppress go ahead.
                        (assert
                         (member option-byte-code
                                 (list +suppress-go-ahead/byte-code+
                                       +echo/byte-code+)))))
                     ((= command-byte-code +do/byte-code+)
                      (let ((option-byte-code (%read-byte)))
                        (send-will-not option-byte-code telnet-stream)))
                     ;; Just read the option byte and ignore it for the
                     ;; moment.
                     (t (%read-byte))))
             else do
             (return (funcall fn byte))))))

;;; The following would remain as not part of the stream interface; i.e.
;;; it would not be rolled into write-char or write-byte.  It might be
;;; fun to change the send prefix to write so that one has write-will.

(defun send-will (option-byte-code telnet-stream)
   (send-option +will/byte-code+ option-byte-code telnet-stream))

(defun send-will-not (option-byte-code telnet-stream)
   (send-option +will-not/byte-code+ option-byte-code telnet-stream))

(defun send-do (option-byte-code telnet-stream)
   (send-option +do/byte-code+ option-byte-code telnet-stream))

(defun send-do-not (option-byte-code telnet-stream)
   (send-option +do-not/byte-code+ option-byte-code telnet-stream))

(defun send-option (command-byte-code option-byte-code telnet-stream)
   (with-accessors ((tcp-stream tcp-stream)) telnet-stream
     (write-byte +iac/byte-code+ tcp-stream)
     (write-byte command-byte-code tcp-stream)
     (write-byte option-byte-code tcp-stream)))

From: ······@gmail.com
Subject: Re: Stumbling towards implementing telnet
Date: 
Message-ID: <1164870272.965530.149380@16g2000cwy.googlegroups.com>
A couple of things that grabbed my attention in your code:

>      (send-do +suppress-go-ahead/byte-code+ object)
>      (send-will +suppress-go-ahead/byte-code+ object)
>      (send-do-not +echo/byte-code+ object)))

You send these telnet option commands immediately
after connecting and you don't keep track of state
at all (your state and the server's state). "DO NOT" is normally
sent as an acknowledgment to a previously sent WILL therefore
if you send it before receiving a WILL, it will most likely be ignored.

> (defun recv-octet (fn telnet-stream &optional eof-error-p eof-value)
>    (with-accessors ((tcp-stream tcp-stream)) telnet-stream
>      (flet ((%read-byte () (read-byte tcp-stream eof-error-p eof-value)))
>        (loop for byte = (%read-byte) while byte
>              if (= byte +iac/byte-code+) do
>              (let ((command-byte-code (%read-byte)))
>                (cond ((= command-byte-code +iac/byte-code+)
>                       ;; Not really a command-code but rather an
>                       ;; escapded data value 255.
>                       (return (funcall fn command-byte-code)))
>                      ((= command-byte-code +will/byte-code+)
>                       (let ((option-byte-code (%read-byte)))
>                         ;; At the moment, we're only ready to "deal"
>                         ;; with somebody accepting our request to
>                         ;; suppress go ahead.
>                         (assert
>                          (member option-byte-code
>                                  (list +suppress-go-ahead/byte-code+
>                                        +echo/byte-code+)))))
>                      ((= command-byte-code +do/byte-code+)
>                       (let ((option-byte-code (%read-byte)))
>                         (send-will-not option-byte-code telnet-stream)))
>                      ;; Just read the option byte and ignore it for the
>                      ;; moment.
>                      (t (%read-byte))))
>              else do
>              (return (funcall fn byte))))))
>
Here i see that you do not deal with telnet sub option negotiation at
all
(IAC SB ..... IAC SE). I think the best way to do what you want is to
create the whole thing as a state machine on a layer of its own.
You definately want to keep track of both states (your own and the
server)
and respond to requests in a timely manner according to your client
preferences
and current state. Read the relevant rfcs (Q method of implementing
telnet option negotiation
is a good one aswell) and try to come up with something that models the
telnet
state machine.
From: Damien Kick
Subject: Re: Stumbling towards implementing telnet
Date: 
Message-ID: <5yDbh.5013$ql2.3461@newsread3.news.pas.earthlink.net>
······@gmail.com wrote:
> A couple of things that grabbed my attention in your code:
> 
>>      (send-do +suppress-go-ahead/byte-code+ object)
>>      (send-will +suppress-go-ahead/byte-code+ object)
>>      (send-do-not +echo/byte-code+ object)))
> 
> You send these telnet option commands immediately
> after connecting and you don't keep track of state
> at all (your state and the server's state). "DO NOT" is normally
> sent as an acknowledgment to a previously sent WILL therefore
> if you send it before receiving a WILL, it will most likely be ignored.

Yeah, I probably shouldn't have put that into the post.  Those three 
lines represent my having just resorted to well, what if I try this?  I 
tried different combinations and what is in the post was the last 
combination I had tried.  As I mentioned in the comments though, having 
them there or not didn't seem to make any difference to the behavior of 
the telnet servers I was using to test.

> Here i see that you do not deal with telnet sub option negotiation at
> all (IAC SB ..... IAC SE).

That is because, for the moment, I always refuse to deal with any option 
at all.  IIRC, one can not start negotiating sub options until the other 
side has agreed to even talk about the option.  I was hoping that I 
could get away with rejecting any and all options and making the server 
deal with an NVT, with the exception of leaving out the go-ahead 
business.  That isn't working for me really well at the moment and I 
know that I have to work on aspects of that.  It is working well enough, 
however, that I am getting something from one of the telnet servers with 
which I've been testing.  What I'm really hoping for, though, is some 
help on understanding how to translate what I've got, even though it is 
too anti-social to be ready for a debutante ball, into the simple-stream 
framework so that I can work on dealing with the rest of options 
negotiation with a telnet-simple-stream as opposed to a telnet-quasi-stream.
From: ······@gmail.com
Subject: Re: Stumbling towards implementing telnet
Date: 
Message-ID: <1164906262.718746.14690@j44g2000cwa.googlegroups.com>
Well, if you're looking for a somewhat solid "ignore everything"
strategy
the following may be of help:

  IAC IAC -> data byte 255
  IAC WILL/WONT/DO/DONT xxx -> ignore
  IAC SB xxx ... IAC SE -> ignore
  IAC other -> ignore

I don't know if its any relevant to what you are trying to do
since i didn't use streams, but the way i implemented telnet
protocol recently is to have a basic socket reader and writer
and a telnet state machine on top of that. The state machine
reads bytes received by the reader (queue), takes care of option
negotiation and filters out the "pure data byte stream" to a queue
that is then read by the application per se. Same for writing.

I tried using your strategy at first, ignore everything and try to
enable
some options i care about (echo, linemode etc) without keeping track
of state but it soon became apparent that it was not going to work.
Extending that later on in order to implement more options turned out
not straightforward so i scrapped everything and wrote the state
machine.

Damien Kick wrote:
> That is because, for the moment, I always refuse to deal with any option
> at all.  IIRC, one can not start negotiating sub options until the other
> side has agreed to even talk about the option.  I was hoping that I
> could get away with rejecting any and all options and making the server
> deal with an NVT, with the exception of leaving out the go-ahead
> business.  That isn't working for me really well at the moment and I
> know that I have to work on aspects of that.  It is working well enough,
> however, that I am getting something from one of the telnet servers with
> which I've been testing.  What I'm really hoping for, though, is some
> help on understanding how to translate what I've got, even though it is
> too anti-social to be ready for a debutante ball, into the simple-stream
> framework so that I can work on dealing with the rest of options
> negotiation with a telnet-simple-stream as opposed to a telnet-quasi-stream.
From: ············@gmail.com
Subject: Re: Stumbling towards implementing telnet
Date: 
Message-ID: <1164916667.872772.240550@h54g2000cwb.googlegroups.com>
Is it my imagination or is Telnet completely fubar in most instances.
I used to work with the protocol years ago and seemed like every server
implementation was totally quirky in its options negotations.  Maybe it
was just me, but I found the whole thing to be flaky at best.
From: Damien Kick
Subject: Re: Stumbling towards implementing telnet
Date: 
Message-ID: <CaPbh.5012$sf5.3859@newsread4.news.pas.earthlink.net>
············@gmail.com wrote:
> Is it my imagination or is Telnet completely fubar in most instances.
>  I used to work with the protocol years ago and seemed like every
> server implementation was totally quirky in its options negotations.
> Maybe it was just me, but I found the whole thing to be flaky at
> best.

Yeah, <grumble/> I'm not having much fun with it.  Kenny, won't you
please summon the Open Source Fairy for me and have it write
telnet-simple-stream for me?

telnet> toggle options
Will show option processing.
telnet> open $HOSTNAME
Trying ...
Connected ...
Escape character is '^]'.
SENT DO SUPPRESS GO AHEAD
SENT WILL TERMINAL TYPE
SENT WILL NAWS
SENT WILL TSPEED
SENT WILL LFLOW
SENT WILL LINEMODE
SENT WILL NEW-ENVIRON
SENT DO STATUS
RCVD WILL ECHO
SENT DO ECHO
RCVD DO TERMINAL TYPE
RCVD WILL SUPPRESS GO AHEAD
RCVD DO NAWS
SENT IAC SB NAWS 0 80 (80) 0 49 (49)
RCVD IAC SB TERMINAL-TYPE SEND
SENT IAC SB TERMINAL-TYPE IS "CYGWIN"
RCVD DONT TSPEED
RCVD DONT LFLOW
RCVD DO LINEMODE
SENT IAC SB LINEMODE SLC SYNCH NOSUPPORT 0; IP VARIABLE|FLUSHIN|FLUSHOUT
3; AO VARIABLE 15; AYT NOSUPPORT 0; ABORT VARIABLE|FLUSHIN|FLUSHOUT 28;
EOF VARIABLE 4; SUSP VARIABLE|FLUSHIN 26; EC VARIABLE 8; EL VARIABLE 21;
EW VARIABLE 23; RP VARIABLE 18; LNEXT VARIABLE 22; XON VARIABLE 17; XOFF
VARIABLE 19; FORW1 NOSUPPORT 0; FORW2 NOSUPPORT 0;

SENT DO SUPPRESS GO AHEAD
RCVD DONT NEW-ENVIRON
RCVD WONT STATUS
RCVD WONT LINEMODE

WELCOME TO  [PORT $ZSAM1 #23 WINDOW $ZTN1.#PTCPMNR]
TELSERV - T9553G06 - (10DEC2004) - (IPMAEA)


Available Services:

TACL     EXIT
Enter Choice>
telnet> close
Connection closed.
telnet>

And mine (so far). I'm slowly trying to add options negotiations being
used by the system telnet hoping that this will help make the telnet
server like me. I tell the server to DO SUPPRESS-GO-AHEAD, like the
system telnet. I adding sending that I WILL TERMINAL-TYPE, like the
system telnet. I'm still telling the server that I WON'T WINDOW-SIZE.
I'll try adding this to see if it makes a difference because it is after
that option that the server asks the system telnet for the terminal
type, a question which it never asks my telnet-quasi-stream.

TELNET-STREAM-USER> (trace)
(NET.EARTHLINK.DKIXK.TELNET-STREAM::SEND-TERMINAL-TYPE
   NET.EARTHLINK.DKIXK.TELNET-STREAM::SEND-DO-NOT
   NET.EARTHLINK.DKIXK.TELNET-STREAM::SEND-DO
   NET.EARTHLINK.DKIXK.TELNET-STREAM::SEND-WILL-NOT
   NET.EARTHLINK.DKIXK.TELNET-STREAM::SEND-WILL
   (LABELS NET.EARTHLINK.DKIXK.TELNET-STREAM::RECV-OCTET
     NET.EARTHLINK.DKIXK.TELNET-STREAM::%RECV-WILL)
   (LABELS NET.EARTHLINK.DKIXK.TELNET-STREAM::RECV-OCTET
     NET.EARTHLINK.DKIXK.TELNET-STREAM::%RECV-DO)
   (LABELS NET.EARTHLINK.DKIXK.TELNET-STREAM::RECV-OCTET
     NET.EARTHLINK.DKIXK.TELNET-STREAM::%RECV-SB))
TELNET-STREAM-USER> (duff-test)
  0[8]: (NET.EARTHLINK.DKIXK.TELNET-STREAM::SEND-DO 3
          #<NET.EARTHLINK.DKIXK.TELNET-STREAM::TELNET-QUASI-STREAM @
            #x2111849a>)
  0[8]: returned 3
  0[8]: ((LABELS NET.EARTHLINK.DKIXK.TELNET-STREAM::RECV-OCTET
           NET.EARTHLINK.DKIXK.TELNET-STREAM::%RECV-WILL)
         1)
Warning: Received unknown option 1.
  0[8]: returned NIL
  0[8]: ((LABELS NET.EARTHLINK.DKIXK.TELNET-STREAM::RECV-OCTET
           NET.EARTHLINK.DKIXK.TELNET-STREAM::%RECV-DO)
         24)
    1[8]: (NET.EARTHLINK.DKIXK.TELNET-STREAM::SEND-WILL 24
            #<NET.EARTHLINK.DKIXK.TELNET-STREAM::TELNET-QUASI-STREAM @
              #x20da255a>)
    1[8]: returned 24
  0[8]: returned T
  0[8]: ((LABELS NET.EARTHLINK.DKIXK.TELNET-STREAM::RECV-OCTET
           NET.EARTHLINK.DKIXK.TELNET-STREAM::%RECV-WILL)
         3)
  0[8]: returned T
  0[8]: ((LABELS NET.EARTHLINK.DKIXK.TELNET-STREAM::RECV-OCTET
           NET.EARTHLINK.DKIXK.TELNET-STREAM::%RECV-DO)
         31)
    1[8]: (NET.EARTHLINK.DKIXK.TELNET-STREAM::SEND-WILL-NOT 31
            #<NET.EARTHLINK.DKIXK.TELNET-STREAM::TELNET-QUASI-STREAM @
              #x20da255a>)
    1[8]: returned 31
  0[8]: returned 31
^M
WELCOME TO  [PORT $ZSAM1 #23 WINDOW $ZTN1.#PTCPMPL] ^M
TELSERV - T9553G06 - (10DEC2004) - (IPMAEA)^M
Available Services:^M
^M
TACL     EXIT     ^M
Enter Choice> TACL^M
; Evaluation aborted
TELNET-STREAM-USER>

There is a really long pause between my sending what I WON'T WINDOW-SIZE
and the receipt of the first data from the server. Seems like a minute
or two. I send TACL to start the login and then it just hangs. Anyway,
the duff test.

(defun duff-test ()
   (with-telnet (server *hostname*)
     (flet
         ;; I'm calling this %read-char and %write-char to emphasize
         ;; that I would like recv-char to really be read-char working
         ;; with a telnet-stream.  They both echo.
         ((%read-char ()
            (prog1 (princ (recv-char server)) (finish-output))
          (%write-char (char)
            (prog1 (send-char char server)
              (princ char) (finish-output))))
       (loop with expected = "Enter Choice> "
             with i = 0
             for received = (%read-char)
             if (eql received (schar expected i)) do (incf i)
             else do (setq i 0)
             while (< i (length expected)))
       (loop for x in '(#\T #\A #\C #\L #\Return #\Linefeed)
             do (%write-char x))
       ;; We never really get this far...
       (loop (%read-char)))))

And it only just occurs to me now that perhaps all I really need to do
is (loop for x ... do (%write-char x) finally (force-output (tcp-stream
server))) to get into the last %read-char loop...

FWIW, the latest code follows.  Again, I'm still mostly hoping to get
some help with how to translate this into simple-streams.  To do the
transformation into Gray streams would be pretty easy, I think.  Just
change recv-char to stream-read-char, etc.  But I can't figure out how
to use the simple-streams APIs to do this stuff.

(in-package :net.earthlink.dkixk.telnet-stream)

(defconstant +iac/byte-code+ 255
   "Integer value for telnet IAC (is a command), used to indicate the
beginning of a sequence used for negotiating telnet options.")

(defconstant +se/byte-code+ 240)
(defconstant +sb/byte-code+ 250)
(defconstant +will/byte-code+ 251)
(defconstant +will-not/byte-code+ 252)
(defconstant +do/byte-code+ 253)
(defconstant +do-not/byte-code+ 254)

;; The following are the telnet options I'm finding either the Solaris
;; telnet server or the Tandem telnet server is trying to negotiate
;; with me.
(defconstant +echo/byte-code+ 1)
(defconstant +suppress-go-ahead/byte-code+ 3)
(defconstant +terminal-type/byte-code+ 24)
(defconstant +window-size/byte-code+ 31) ;aka NAWS
(defconstant +xdisploc/byte-code+ 35)
(defconstant +environ/byte-code+ 36)
(defconstant +newenv/byte-code+ 39)

(defclass telnet-quasi-stream ()
     ((tcp-stream :reader tcp-stream)
      ;; The state machine can be embedded in the implementation of the
      ;; stream.
      (suppress-go-ahead :initform nil)
      (wait-for-terminal-type-suboption :initform nil)))

(defmethod initialize-instance :after
     ((object telnet-quasi-stream)
      &key remote-host (remote-port (lookup-port :telnet :tcp)))
   (with-slots (tcp-stream) object
     (setq tcp-stream (make-socket :remote-host remote-host
                                   :remote-port remote-port))
     (send-do +suppress-go-ahead/byte-code+ object)))

(defun telnet (remote-host &optional
                            (remote-port (lookup-port :telnet :tcp)))
   (make-instance 'telnet-quasi-stream
                  :remote-host remote-host :remote-port remote-port))

(defmethod close ((stream telnet-quasi-stream) &key abort)
   (with-slots (tcp-stream
                suppress-go-ahead
                wait-for-terminal-type-suboption)
       stream
     (when tcp-stream
       (prog1 (close tcp-stream :abort abort)
         (setq tcp-stream nil)
         (setq suppress-go-ahead nil)
         (setq wait-for-terminal-type-suboption nil)))))

(defun send-char (char telnet-stream)
   (with-slots (tcp-stream) telnet-stream
     (assert (swank::ascii-char-p char))
     (write-char char tcp-stream)))

(defun send-byte (byte telnet-stream)
   #-(and) (assert (transmit-binary telnet-stream))
   (when (= byte +iac/byte-code+)
     (write-byte +iac/byte-code+ (tcp-stream telnet-stream)))
   (write-byte byte (tcp-stream telnet-stream)))

(defun recv-char (stream &optional eof-error-p eof-value)
   (recv-octet #'code-char stream eof-error-p eof-value))

(defun recv-byte (stream &optional eof-error-p eof-value)
   (recv-octet #'identity stream eof-error-p eof-value))

(defun recv-octet (fn telnet-stream &optional eof-error-p eof-value)
   (with-slots (tcp-stream
                suppress-go-ahead
                wait-for-terminal-type-suboption)
       telnet-stream
     (labels
         ((%read-byte () (read-byte tcp-stream eof-error-p eof-value))
          (%recv-will (option-byte-code)
            (cond ((= option-byte-code +suppress-go-ahead/byte-code+)
                   (setq suppress-go-ahead t))
                  (t (warn "Received unknown option ~S."
                           option-byte-code))))
          (%recv-do (option-byte-code)
            (cond ((= option-byte-code +terminal-type/byte-code+)
                   (send-will +terminal-type/byte-code+ telnet-stream)
                   (setq wait-for-terminal-type-suboption t))
                  (t (as send-will-not option-byte-code telnet-stream))))
          (%recv-sb (option-byte-code)
            (cond ((= option-byte-code +terminal-type/byte-code+)
                   (unless wait-for-terminal-type-suboption
                     (warn "Received SB ~S before having sent a ~
                            WILL TERMINAL-TYPE"
                            option-byte-code))
                   (let ((send/byte-code 1))
                     (when (= (%read-byte) send/byte-code)
                       (assert (and (= (%read-byte) +iac/byte-code+)
                                    (= (%read-byte) +se/byte-code+)))
                       (send-terminal-type telnet-stream))))
                  (t (warn "Received unknown SB option ~S."
                           option-byte-code)
                     (loop for octet = (%read-byte)
                           ;; Should probably really look for
                           ;; (+iac/byte-cote+ +se/byte-code+)
                           until (or (not octet)
                                     (= octet +se/byte-code+)))))))
       (loop for byte = (%read-byte) while byte
             if (= byte +iac/byte-code+) do
             (let ((command-byte-code (%read-byte)))
               (cond ((= command-byte-code +iac/byte-code+)
                      ;; Not really a command-code but rather an
                      ;; escapded data value 255.
                      (return (funcall fn command-byte-code)))
                     ((= command-byte-code +will/byte-code+)
                      (%recv-will (%read-byte)))
                     ((= command-byte-code +do/byte-code+)
                      (%recv-do (%read-byte)))
                     ((= command-byte-code +sb/byte-code+)
                      (%recv-sb (%read-byte)))
                     ;; Just read the option byte and ignore it for the
                     ;; moment.
                     (t (let ((unknown (%read-byte)))
                          (warn "Received unknown byte ~S." unknown)))))
             else do
             (unless suppress-go-ahead
               (warn "Have received data before WILL ~
                      SUPPRESS-GO-AHEAD."))
             (return (funcall fn byte))))))

(defun send-will (option-byte-code telnet-stream)
   (send-command +will/byte-code+ option-byte-code telnet-stream))

(defun send-will-not (option-byte-code telnet-stream)
   (send-command +will-not/byte-code+ option-byte-code telnet-stream))

(defun send-do (option-byte-code telnet-stream)
   (send-command +do/byte-code+ option-byte-code telnet-stream))

(defun send-do-not (option-byte-code telnet-stream)
   (send-command +do-not/byte-code+ option-byte-code telnet-stream))

(defun send-command (command-byte-code option-byte-code telnet-stream)
   (with-accessors ((tcp-stream tcp-stream)) telnet-stream
     (write-byte +iac/byte-code+ tcp-stream)
     (write-byte command-byte-code tcp-stream)
     (write-byte option-byte-code tcp-stream)))

(defun send-terminal-type (telnet-stream)
   (with-slots (tcp-stream) telnet-stream
     (flet ((%write-byte (byte) (write-byte byte tcp-stream))
            (%write-string (string) (write-string string tcp-stream)))
       (let ((is/byte-code 0))
         (%write-byte +iac/byte-code+)
         (%write-byte +sb/byte-code+)
         (%write-byte +terminal-type/byte-code+)
         (%write-byte is/byte-code)
         (%write-string "NETWORK-VIRTUAL-TERMINAL")
         (%write-byte +iac/byte-code+)
         (%write-byte +se/byte-code+)))))
From: ······@gmail.com
Subject: Re: Stumbling towards implementing telnet
Date: 
Message-ID: <1165001104.108164.228900@j44g2000cwa.googlegroups.com>
NAWS support shouldn't matter (marked SHOULD but not MUST in rfc1123)
When negotiating suboptions you should still check for double IAC.

I think right now you stop "eating" suboption bytes when you read the
first IAC.
From: ······@gmail.com
Subject: Re: Stumbling towards implementing telnet
Date: 
Message-ID: <1165001755.041207.227930@l12g2000cwl.googlegroups.com>
Also:

Log every request you receive and every reply you send
and post them. It will be easier to see if something is wrong
in your option negotiation.
From: John Thingstad
Subject: Re: Stumbling towards implementing telnet
Date: 
Message-ID: <op.tjvv4dxcpqzri1@pandora.upc.no>
> Yeah, <grumble/> I'm not having much fun with it.  Kenny, won't you
> please summon the Open Source Fairy for me and have it write
> telnet-simple-stream for me?

What for! telnet is insecure. These days use SSH.
Much more worth the effort. Also forget FTP.

-- 
Using Opera's revolutionary e-mail client: http://www.opera.com/mail/
From: dkixk
Subject: Re: Stumbling towards implementing telnet
Date: 
Message-ID: <1165010874.099817.178760@73g2000cwn.googlegroups.com>
John Thingstad wrote:
> > Yeah, <grumble/> I'm not having much fun with it.  Kenny, won't you
> > please summon the Open Source Fairy for me and have it write
> > telnet-simple-stream for me?
>
> What for! telnet is insecure. These days use SSH.
> Much more worth the effort. Also forget FTP.

Hey, looks like my realization in a previous post in the thread that I
needed to force output was correct.  I bet that my added negotiating a
terminal-type of "NETWORK-VIRTUAL-TERMINAL" was superfluous.

USER> (with-telnet (server *hostname*)
        (loop (princ (recv-char server))))
^M
^M
SunOS 5.8^M
···@^M
···@
Warning: Received unknown option 1.
login:
; Evaluation aborted

USER> (with-telnet (server *hostname*)
        (loop (princ (recv-char server))))
Warning: Received unknown option 1.
^M
WELCOME TO  [PORT $ZSAM1 #23 WINDOW $ZTN1.#PTCPMZ0] ^M
TELSERV - T9553G06 - (10DEC2004) - (IPMAEA)^M
^M
^G^M
Available Services:^M
^M
TACL     EXIT     ^M
Enter Choice>
; Evaluation aborted

USER>

Now that I have it more working than not, I'll be sure to never use it
again <grin/>.