require_relative 'telnet/codes' # rfc1143 state machine for telnet protocol # Thehandle_ functions handle input, # the send_ functions are for sending negotiations class RFC1143 include Telnet::Codes attr_reader :telopt attr_reader :us attr_reader :him attr_reader :agree def initialize(to, u, h, a) @telopt = to @us = u @him = h @agree = a end =begin EXAMPLE STATE MACHINE FOR THE Q METHOD OF IMPLEMENTING TELNET OPTION NEGOTIATION There are two sides, we (us) and he (him). We keep four variables: us: state of option on our side (NO/WANTNO/WANTYES/YES) usq: a queue bit (EMPTY/OPPOSITE) if us is WANTNO or WANTYES him: state of option on his side himq: a queue bit if him is WANTNO or WANTYES An option is enabled if and only if its state is YES. Note that us/usq and him/himq could be combined into two six-choice states. "Error" below means that producing diagnostic information may be a good idea, though it isn't required. Upon receipt of WILL, we choose based upon him and himq: NO If we agree that he should enable, him=YES, send DO; otherwise, send DONT. YES Ignore. WANTNO EMPTY Error: DONT answered by WILL. him=NO. OPPOSITE Error: DONT answered by WILL. him=YES*, himq=EMPTY. WANTYES EMPTY him=YES. OPPOSITE him=WANTNO, himq=EMPTY, send DONT. * This behavior is debatable; DONT will never be answered by WILL over a reliable connection between TELNETs compliant with this RFC, so this was chosen (1) not to generate further messages, because if we know we're dealing with a noncompliant TELNET we shouldn't trust it to be sensible; (2) to empty the queue sensibly. =end def handle_will case @us when :no if @agree return TELNET_DO, @telopt else return TELNET_DONT, @telopt end when :yes # ignore return nil, nil when :wantno @him = :no return :error, "DONT answered by WILL" when :wantno_opposite @him = :yes return :error, "DONT answered by WILL" when :wantyes @him = :yes return nil, nil when :wantyes_opposite @him = :wantno return TELNET_DONT, @telopt end end =begin Upon receipt of WONT, we choose based upon him and himq: NO Ignore. YES him=NO, send DONT. WANTNO EMPTY him=NO. OPPOSITE him=WANTYES, himq=NONE, send DO. WANTYES EMPTY him=NO.* OPPOSITE him=NO, himq=NONE.** * Here is the only spot a length-two queue could be useful; after a WILL negotiation was refused, a queue of WONT WILL would mean to request the option again. This seems of too little utility and too much potential waste; there is little chance that the other side will change its mind immediately. ** Here we don't have to generate another request because we've been "refused into" the correct state anyway. =end def handle_wont case @us when :no return nil, nil when :yes @him = :no return TELNET_DONT, @telopt when :wantno @him = :no return nil, nil when :wantno_opposite @him = :wantyes return TELNET_DO, @telopt when :wantyes @him = :no return nil, nil when :wantyes_opposite @him = :no return nil, nil end end =begin If we decide to ask him to enable: NO him=WANTYES, send DO. YES Error: Already enabled. WANTNO EMPTY If we are queueing requests, himq=OPPOSITE; otherwise, Error: Cannot initiate new request in the middle of negotiation. OPPOSITE Error: Already queued an enable request. WANTYES EMPTY Error: Already negotiating for enable. OPPOSITE himq=EMPTY. We handle the option on our side by the same procedures, with DO- WILL, DONT-WONT, him-us, himq-usq swapped. =end def handle_do case @him when :no @us = :wantyes return TELNET_WILL, @telopt when :yes return :error, 'Already enabled' when :wantno # us = :wantno_opposite # only if "buffering", whatever that means. return :error, 'Request in the middle of negotiation' when :wantno_opposite return :error, 'Already queued request' when :wantyes return :error, 'Already negotiating for enable' when :wantyes_opposite @us = :wantyes return nil, nil end end =begin If we decide to ask him to disable: NO Error: Already disabled. YES him=WANTNO, send DONT. WANTNO EMPTY Error: Already negotiating for disable. OPPOSITE himq=EMPTY. WANTYES EMPTY If we are queueing requests, himq=OPPOSITE; otherwise, Error: Cannot initiate new request in the middle of negotiation. OPPOSITE Error: Already queued a disable request. We handle the option on our side by the same procedures, with DO- WILL, DONT-WONT, him-us, himq-usq swapped. =end def handle_dont case @him when :no return :error, 'Already disabled' when :yes @us = :wantno return TELNET_WONT, @telopt when :wantno return :error, 'Already negotiating for disable' when :wantno_opposite @us = :wantno return nil, nil when :wantyes # us = :wantno_opposite # only if "buffering", whatever that means. return :error, 'Request in the middle of negotiation' when :wantyes_opposite return :error, 'Already queued disable request' end end # advertise willingess to support an option def send_will case @us when :no @us = :wantyes return TELNET_WILL, @telopt when :wantno @us = :wantno_opposite when :wantyes_opposite @us = :wantyes else return nil, nil end end # force turn-off of locally enabled option def send_wont case @us when :yes @us = :wantno return TELNET_WONT, @telopt when :wantno_opposite @us = :wantno return nil, nil when :wantyes @us = :wantyes_opposite return nil, nil else return nil, nil end end # ask remote end to enable an option def send_do case @him when :no @him = :wantyes return TELNET_DO, @telopt when :wantno @him = :wantno_opposite return nil, nil when :wantyes_opposite @us = :wantyes return nil, nil else return nil, nil end end # demand remote end disable an option def send_dont case @him when :yes @him = :wantno return TELNET_DONT, @telopt when :wantno_opposite @him = :wantno return nil, nil when :wantyes @him = :wantyes_opposite else return nil, nil end end end