|
@@ -4,6 +4,8 @@ require 'timeout'
|
|
|
|
|
|
require_relative '../monolog'
|
|
require_relative '../monolog'
|
|
require_relative '../telnet'
|
|
require_relative '../telnet'
|
|
|
|
+require_relative 'account'
|
|
|
|
+
|
|
|
|
|
|
module Woe
|
|
module Woe
|
|
|
|
|
|
@@ -39,6 +41,12 @@ class Client
|
|
@io.close
|
|
@io.close
|
|
end
|
|
end
|
|
|
|
|
|
|
|
+ # Is the client in read timeout state
|
|
|
|
+ def timeout?
|
|
|
|
+ return false unless @timeout_at
|
|
|
|
+ return Time.now >= @timeout_at
|
|
|
|
+ end
|
|
|
|
+
|
|
def alive?
|
|
def alive?
|
|
@fiber.alive? && @busy
|
|
@fiber.alive? && @busy
|
|
end
|
|
end
|
|
@@ -49,6 +57,7 @@ class Client
|
|
|
|
|
|
def write_raw(data)
|
|
def write_raw(data)
|
|
@io.write(data)
|
|
@io.write(data)
|
|
|
|
+ @io.flush
|
|
end
|
|
end
|
|
|
|
|
|
|
|
|
|
@@ -56,7 +65,10 @@ class Client
|
|
@telnet.send_escaped(data)
|
|
@telnet.send_escaped(data)
|
|
end
|
|
end
|
|
|
|
|
|
-
|
|
|
|
|
|
+ def printf(fmt, *args)
|
|
|
|
+ @telnet.printf(fmt, *args)
|
|
|
|
+ end
|
|
|
|
+
|
|
def on_start
|
|
def on_start
|
|
p "Starting client fiber"
|
|
p "Starting client fiber"
|
|
return nil
|
|
return nil
|
|
@@ -85,12 +97,10 @@ class Client
|
|
def telnet_event(type, *data)
|
|
def telnet_event(type, *data)
|
|
# store in the event queue
|
|
# store in the event queue
|
|
@telnet_events << TelnetEvent.new(type, data)
|
|
@telnet_events << TelnetEvent.new(type, data)
|
|
- p "Received tenet event #{@telnet_events}."
|
|
|
|
|
|
+ log_debug("Received tenet event #{@telnet_events}.")
|
|
end
|
|
end
|
|
|
|
|
|
def telnet_send_data(buf)
|
|
def telnet_send_data(buf)
|
|
- # @telnet_events << TelnetEvent.new(:command, buf)
|
|
|
|
- p "Sending telnet data."
|
|
|
|
self.write_raw(buf)
|
|
self.write_raw(buf)
|
|
end
|
|
end
|
|
|
|
|
|
@@ -124,6 +134,7 @@ class Client
|
|
end
|
|
end
|
|
|
|
|
|
unless @telnet_events.empty?
|
|
unless @telnet_events.empty?
|
|
|
|
+ @timeout_at = nil
|
|
return @telnet_events.shift
|
|
return @telnet_events.shift
|
|
end
|
|
end
|
|
|
|
|
|
@@ -139,12 +150,13 @@ class Client
|
|
data = on_read
|
|
data = on_read
|
|
# all data ends up in he telnet_events queue
|
|
# all data ends up in he telnet_events queue
|
|
unless @telnet_events.empty?
|
|
unless @telnet_events.empty?
|
|
|
|
+ @timeout_at = nil
|
|
return @telnet_events.shift
|
|
return @telnet_events.shift
|
|
end
|
|
end
|
|
when :write
|
|
when :write
|
|
on_write(arg)
|
|
on_write(arg)
|
|
else
|
|
else
|
|
- p "Unknown command #{cmd}"
|
|
|
|
|
|
+ log_warning("Unknown command #{cmd}")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
@@ -173,41 +185,10 @@ class Client
|
|
end
|
|
end
|
|
|
|
|
|
|
|
|
|
- def ask_login
|
|
|
|
- @login = nil
|
|
|
|
- while @login.nil? || @login.empty?
|
|
|
|
- write("Login:")
|
|
|
|
- @login = wait_for_command
|
|
|
|
- end
|
|
|
|
- @login.chomp!
|
|
|
|
- true
|
|
|
|
- end
|
|
|
|
-
|
|
|
|
- def ask_password
|
|
|
|
- @password = nil
|
|
|
|
- while @password.nil? || @password.empty?
|
|
|
|
- write("\r\nPassword:")
|
|
|
|
- @password = wait_for_command
|
|
|
|
- end
|
|
|
|
- @password.chomp!
|
|
|
|
- true
|
|
|
|
- end
|
|
|
|
-
|
|
|
|
- def handle_command
|
|
|
|
- order = wait_for_command
|
|
|
|
- case order
|
|
|
|
- when "/quit"
|
|
|
|
- write("Byebye!\r\n")
|
|
|
|
- @busy = false
|
|
|
|
- else
|
|
|
|
- @server.broadcast("#@login said #{order}\r\n")
|
|
|
|
- end
|
|
|
|
- end
|
|
|
|
-
|
|
|
|
# generic negotiation
|
|
# generic negotiation
|
|
def setup_negotiate(command, option, yes_event, no_event)
|
|
def setup_negotiate(command, option, yes_event, no_event)
|
|
@telnet.telnet_send_negotiate(command, option)
|
|
@telnet.telnet_send_negotiate(command, option)
|
|
- tev = wait_for_input(0.5)
|
|
|
|
|
|
+ tev = wait_for_input(1.0)
|
|
return false, nil unless tev
|
|
return false, nil unless tev
|
|
return false, nil if tev.type == no_event
|
|
return false, nil if tev.type == no_event
|
|
return false, tev unless tev.type == yes_event && tev.data[0] == option
|
|
return false, tev unless tev.type == yes_event && tev.data[0] == option
|
|
@@ -220,16 +201,18 @@ class Client
|
|
return tev unless ok
|
|
return tev unless ok
|
|
@telnet.telnet_begin_compress2
|
|
@telnet.telnet_begin_compress2
|
|
log_info("Client #{@id} started COMPRESS2 compression")
|
|
log_info("Client #{@id} started COMPRESS2 compression")
|
|
|
|
+ @support_compress2 = true
|
|
end
|
|
end
|
|
|
|
|
|
# Negotiate NAWS (window size) support
|
|
# Negotiate NAWS (window size) support
|
|
def setup_naws
|
|
def setup_naws
|
|
ok, tev = setup_negotiate(TELNET_DO, TELNET_TELOPT_NAWS, :will, :wont)
|
|
ok, tev = setup_negotiate(TELNET_DO, TELNET_TELOPT_NAWS, :will, :wont)
|
|
return tev unless ok
|
|
return tev unless ok
|
|
- tev2 = wait_for_input(0.5)
|
|
|
|
|
|
+ tev2 = wait_for_input(1.0)
|
|
return tev2 unless tev2 && tev2.type == :naws
|
|
return tev2 unless tev2 && tev2.type == :naws
|
|
@window_h, @window_w = *tev2.data
|
|
@window_h, @window_w = *tev2.data
|
|
log_info("Client #{@id} window size #{@window_w}x#{@window_h}")
|
|
log_info("Client #{@id} window size #{@window_w}x#{@window_h}")
|
|
|
|
+ @support_naws = true
|
|
return nil
|
|
return nil
|
|
end
|
|
end
|
|
|
|
|
|
@@ -240,13 +223,116 @@ class Client
|
|
return tev unless ok
|
|
return tev unless ok
|
|
mssp = @server.mssp
|
|
mssp = @server.mssp
|
|
@telnet.telnet_send_mssp(mssp)
|
|
@telnet.telnet_send_mssp(mssp)
|
|
|
|
+ log_info("Client #{@id} accepts MSSP.")
|
|
|
|
+ @support_mssp = true
|
|
return nil
|
|
return nil
|
|
end
|
|
end
|
|
|
|
|
|
|
|
+ # Check for MXP (html-like) support (but don't implement it yet)
|
|
|
|
+ def setup_mxp
|
|
|
|
+ ok, tev = setup_negotiate(TELNET_DO, TELNET_TELOPT_MXP, :will, :wont)
|
|
|
|
+ return tev unless ok
|
|
|
|
+ log_info("Client #{@id} supports MXP.")
|
|
|
|
+ @support_mxp = true
|
|
|
|
+ end
|
|
|
|
+
|
|
|
|
+ # Check for MSP (sound) support (but don't implement it yet)
|
|
|
|
+ def setup_msp
|
|
|
|
+ ok, tev = setup_negotiate(TELNET_DO, TELNET_TELOPT_MSP, :will, :wont)
|
|
|
|
+ return tev unless ok
|
|
|
|
+ log_info("Client #{@id} supports MSP.")
|
|
|
|
+ @support_msp = true
|
|
|
|
+ end
|
|
|
|
+
|
|
|
|
+ # check for MSDP support (extendedboth-way MSSP) but don't support it yet
|
|
|
|
+ def setup_msdp
|
|
|
|
+ ok, tev = setup_negotiate(TELNET_WILL, TELNET_TELOPT_MSDP, :do, :dont)
|
|
|
|
+ return tev unless ok
|
|
|
|
+ mssp = @server.mssp
|
|
|
|
+ @telnet.telnet_send_mssp(mssp)
|
|
|
|
+ log_info("Client #{@id} accepts MSDP.")
|
|
|
|
+ @support_msdp = true
|
|
|
|
+ end
|
|
|
|
|
|
|
|
+ # Negotiate MTTS/TTYPE (TERMINAL TYPE) support
|
|
|
|
+ def setup_ttype
|
|
|
|
+ @terminals = []
|
|
|
|
+ ok, tev = setup_negotiate(TELNET_DO, TELNET_TELOPT_TTYPE, :will, :wont)
|
|
|
|
+ p "ttype 1 #{tev} #{ok}"
|
|
|
|
+ return tev unless ok
|
|
|
|
+ last = "none"
|
|
|
|
+ now = ""
|
|
|
|
+ p "ttype 2"
|
|
|
|
+ until last == now
|
|
|
|
+ last = now
|
|
|
|
+ @telnet.telnet_ttype_send()
|
|
|
|
+ tev2 = nil
|
|
|
|
+ # Some clients (like KildClient, but not TinTin or telnet),
|
|
|
|
+ # insist on spamming useless NUL characters
|
|
|
|
+ # here... So we have to retry a few times to get a ttype_is
|
|
|
|
+ # throwing away any undesirable junk in between.
|
|
|
|
+ 3.times do
|
|
|
|
+ tev2 = wait_for_input(1.0)
|
|
|
|
+ break if tev2 && tev2.type == :ttype_is
|
|
|
|
+ end
|
|
|
|
+ p "ttype 3 #{tev2}"
|
|
|
|
+ return tev2 unless tev2 && tev2.type == :ttype_is
|
|
|
|
+ now = tev2.data.first
|
|
|
|
+ @terminal = now
|
|
|
|
+ @terminals << now unless @terminals.member?(now)
|
|
|
|
+ end
|
|
|
|
+ log_info "Client #{@id} supported terminals #{@terminals}"
|
|
|
|
+ mtts_term = @terminals.find { |t| t =~ /MTTS / }
|
|
|
|
+ if mtts_term
|
|
|
|
+ @mtts = mtts_term.split(" ").last.to_i rescue nil
|
|
|
|
+ log_info "Client #{@id} supports MTTS #{@mtts}" if @mtts
|
|
|
|
+ end
|
|
|
|
+ @support_ttype = true
|
|
|
|
+ return nil
|
|
|
|
+ end
|
|
|
|
+
|
|
|
|
+ # Switches to "password" mode.
|
|
|
|
+ def password_mode
|
|
|
|
+ # The server sends "IAC WILL ECHO", meaning "I, the server, will do any
|
|
|
|
+ # echoing from now on." The client should acknowledge this with an IAC DO
|
|
|
|
+ # ECHO, and then stop putting echoed text in the input buffer.
|
|
|
|
+ # It should also do whatever is appropriate for password entry to the input
|
|
|
|
+ # box thing - for example, it might * it out. Text entered in server-echoes
|
|
|
|
+ # mode should also not be placed any command history.
|
|
|
|
+ # don't use the Q state machne for echos
|
|
|
|
+ @telnet.telnet_send_bytes(TELNET_IAC, TELNET_WILL, TELNET_TELOPT_ECHO)
|
|
|
|
+ tev = wait_for_input(0.1)
|
|
|
|
+ return tev if tev && tev.type != :do
|
|
|
|
+ return nil
|
|
|
|
+ end
|
|
|
|
+
|
|
|
|
+ # Switches to "normal, or non-password mode.
|
|
|
|
+ def normal_mode
|
|
|
|
+ # When the server wants the client to start local echoing again, it sends
|
|
|
|
+ # "IAC WONT ECHO" - the client must respond to this with "IAC DONT ECHO".
|
|
|
|
+ # Again don't use Q state machine.
|
|
|
|
+ @telnet.telnet_send_bytes(TELNET_IAC, TELNET_WONT, TELNET_TELOPT_ECHO)
|
|
|
|
+ tev = wait_for_input(0.1)
|
|
|
|
+ return tev if tev && tev.type != :dont
|
|
|
|
+ return nil
|
|
|
|
+ end
|
|
|
|
+
|
|
|
|
+ def color_test
|
|
|
|
+ self.write("\e[1mBold\e[0m\r\n")
|
|
|
|
+ self.write("\e[3mItalic\e[0m\r\n")
|
|
|
|
+ self.write("\e[4mUnderline\e[0m\r\n")
|
|
|
|
+ 30.upto(37) do | fg |
|
|
|
|
+ self.write("\e[#{fg}mForeground Color #{fg}\e[0m\r\n")
|
|
|
|
+ self.write("\e[1;#{fg}mBold Foreground Color #{fg}\e[0m\r\n")
|
|
|
|
+ end
|
|
|
|
+ 40.upto(47) do | bg |
|
|
|
|
+ self.write("\e[#{bg}mBackground Color #{bg}\e[0m\r\n")
|
|
|
|
+ self.write("\e[1;#{bg}mBold Background Color #{bg}\e[0m\r\n")
|
|
|
|
+ end
|
|
|
|
+ end
|
|
|
|
|
|
def setup_telnet
|
|
def setup_telnet
|
|
- loop do
|
|
|
|
|
|
+ loop do
|
|
tev = wait_for_input(0.5)
|
|
tev = wait_for_input(0.5)
|
|
if tev
|
|
if tev
|
|
p "setup_telnet", tev
|
|
p "setup_telnet", tev
|
|
@@ -258,6 +344,12 @@ class Client
|
|
setup_mssp
|
|
setup_mssp
|
|
setup_compress2
|
|
setup_compress2
|
|
setup_naws
|
|
setup_naws
|
|
|
|
+ setup_ttype
|
|
|
|
+ setup_mxp
|
|
|
|
+ setup_msp
|
|
|
|
+ setup_msdp
|
|
|
|
+ # color_test
|
|
|
|
+
|
|
|
|
|
|
#p "mssp ev #{tev}"
|
|
#p "mssp ev #{tev}"
|
|
# @telnet.telnet_send_negotiate(TELNET_WILL, TELNET_TELOPT_MSSP)
|
|
# @telnet.telnet_send_negotiate(TELNET_WILL, TELNET_TELOPT_MSSP)
|
|
@@ -268,14 +360,93 @@ class Client
|
|
|
|
|
|
|
|
|
|
end
|
|
end
|
|
|
|
+
|
|
|
|
+ LOGIN_RE = /\A[A-Za-z][A-Za-z0-9]*\Z/
|
|
|
|
+
|
|
|
|
+ def ask_something(prompt, re, nomatch_prompt)
|
|
|
|
+ something = nil
|
|
|
|
|
|
|
|
+ while something.nil? || something.empty?
|
|
|
|
+ write("#{prompt}:")
|
|
|
|
+ something = wait_for_command
|
|
|
|
+ if something
|
|
|
|
+ something.chomp!
|
|
|
|
+ if re && something !~ re
|
|
|
|
+ write("\r\n#{nomatch_prompt}\r\n")
|
|
|
|
+ something = nil
|
|
|
|
+ end
|
|
|
|
+ end
|
|
|
|
+ end
|
|
|
|
+ something.chomp!
|
|
|
|
+ true
|
|
|
|
+ end
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ def ask_login
|
|
|
|
+ return ask_something("Login", LOGIN_RE, "Login must consist of a letter followed by letters or numbers.")
|
|
|
|
+ end
|
|
|
|
+
|
|
|
|
+ EMAIL_RE = /@/
|
|
|
|
+
|
|
|
|
+ def ask_email
|
|
|
|
+ return ask_something("E-mail:", EMAIL_RE, "Email must have at least an @ in there somewhere.")
|
|
|
|
+ end
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ def ask_password(prompt = "Password")
|
|
|
|
+ password = nil
|
|
|
|
+ password_mode
|
|
|
|
+ while password.nil? || password.empty?
|
|
|
|
+ write("\r\n#{prompt}:")
|
|
|
|
+ password = wait_for_command
|
|
|
|
+ end
|
|
|
|
+ password.chomp!
|
|
|
|
+ normal_mode
|
|
|
|
+ true
|
|
|
|
+ end
|
|
|
|
+
|
|
|
|
+ def handle_command
|
|
|
|
+ order = wait_for_command
|
|
|
|
+ case order
|
|
|
|
+ when "/quit"
|
|
|
|
+ write("Byebye!\r\n")
|
|
|
|
+ @busy = false
|
|
|
|
+ else
|
|
|
|
+ @server.broadcast("#@login said #{order}\r\n")
|
|
|
|
+ end
|
|
|
|
+ end
|
|
|
|
+
|
|
def serve()
|
|
def serve()
|
|
setup_telnet
|
|
setup_telnet
|
|
- data = nil
|
|
|
|
- lok = ask_login
|
|
|
|
- return false unless lok
|
|
|
|
- pok = ask_password
|
|
|
|
- return false unless pok
|
|
|
|
|
|
+ login = ask_login
|
|
|
|
+ return false unless login
|
|
|
|
+ @account = Account.fetch(login)
|
|
|
|
+ if @account
|
|
|
|
+ pass = ask_password
|
|
|
|
+ return false unless pass
|
|
|
|
+
|
|
|
|
+ else
|
|
|
|
+ while !@account
|
|
|
|
+ printf("\nWelcome, %s! Creating new account...\n", login)
|
|
|
|
+ pass1 = ask_password
|
|
|
|
+ return false unless pass
|
|
|
|
+ pass2 = ask_password("Repeat Password:")
|
|
|
|
+ return false unless pass
|
|
|
|
+ if pass1 != pass2
|
|
|
|
+ printf("\nPasswords do not match.\n")
|
|
|
|
+ next
|
|
|
|
+ end
|
|
|
|
+ email = ask_email
|
|
|
|
+ return false unless email
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ end
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ end
|
|
|
|
+
|
|
write("\r\nWelcome #{@login} #{@password}!\r\n")
|
|
write("\r\nWelcome #{@login} #{@password}!\r\n")
|
|
while @busy do
|
|
while @busy do
|
|
handle_command
|
|
handle_command
|