Initial commit: extracted from impostor-bbs gems/bbs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
10
bbs.gemspec
Normal file
10
bbs.gemspec
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Gem::Specification.new do |s|
|
||||||
|
s.name = 'bbs'
|
||||||
|
s.version = '0.1.0'
|
||||||
|
s.summary = 'Universal telnet BBS server library'
|
||||||
|
s.author = 'Zsolt Tasnadi'
|
||||||
|
s.files = Dir['lib/**/*.rb']
|
||||||
|
s.require_paths = ['lib']
|
||||||
|
s.required_ruby_version = '>= 3.0'
|
||||||
|
s.add_dependency 'artii', '~> 2.1'
|
||||||
|
end
|
||||||
25
lib/bbs.rb
Normal file
25
lib/bbs.rb
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative 'bbs/config'
|
||||||
|
require_relative 'bbs/telnet'
|
||||||
|
require_relative 'bbs/banner'
|
||||||
|
require_relative 'bbs/flow'
|
||||||
|
require_relative 'bbs/flow_runner'
|
||||||
|
require_relative 'bbs/renderer'
|
||||||
|
require_relative 'bbs/store'
|
||||||
|
require_relative 'bbs/session'
|
||||||
|
require_relative 'bbs/server'
|
||||||
|
|
||||||
|
module BBS
|
||||||
|
def self.configure
|
||||||
|
yield(@config ||= Config.new)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.config
|
||||||
|
@config ||= Config.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.start(port: (ENV['BBS_PORT'] || 2323).to_i)
|
||||||
|
Server.new(port: port).run
|
||||||
|
end
|
||||||
|
end
|
||||||
23
lib/bbs/banner.rb
Normal file
23
lib/bbs/banner.rb
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'artii'
|
||||||
|
|
||||||
|
module BBS
|
||||||
|
module Banner
|
||||||
|
def self.render(text, color: "\e[1;32m", padding: 2)
|
||||||
|
inner = "#{' ' * padding}#{text}#{' ' * padding}"
|
||||||
|
bar = '═' * inner.length
|
||||||
|
rst = "\e[0m"
|
||||||
|
"\r\n #{color}╔#{bar}╗#{rst}\r\n" \
|
||||||
|
" #{color}║#{inner}║#{rst}\r\n" \
|
||||||
|
" #{color}╚#{bar}╝#{rst}\r\n\r\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.render_big(text, color: "\e[1;32m", font: 'slant')
|
||||||
|
rst = "\e[0m"
|
||||||
|
art = Artii::Base.new(font: font).asciify(text)
|
||||||
|
lines = art.split("\n").map { |l| " #{color}#{l}#{rst}" }
|
||||||
|
"\r\n#{lines.join("\r\n")}\r\n\r\n"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
7
lib/bbs/config.rb
Normal file
7
lib/bbs/config.rb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module BBS
|
||||||
|
class Config
|
||||||
|
attr_accessor :screens_dir, :flow
|
||||||
|
end
|
||||||
|
end
|
||||||
89
lib/bbs/flow.rb
Normal file
89
lib/bbs/flow.rb
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module BBS
|
||||||
|
class Flow
|
||||||
|
attr_reader :steps
|
||||||
|
|
||||||
|
def self.define(&block)
|
||||||
|
flow = new
|
||||||
|
flow.instance_eval(&block)
|
||||||
|
flow.freeze
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@steps = []
|
||||||
|
end
|
||||||
|
|
||||||
|
def screen(name, **vars)
|
||||||
|
@steps << { type: :screen, name: name, vars: vars }
|
||||||
|
end
|
||||||
|
|
||||||
|
def banner(text, style: :success)
|
||||||
|
@steps << { type: :banner, text: text, style: style }
|
||||||
|
end
|
||||||
|
|
||||||
|
def big_banner(text, style: :success, font: 'slant')
|
||||||
|
@steps << { type: :big_banner, text: text, style: style, font: font }
|
||||||
|
end
|
||||||
|
|
||||||
|
def say(text, style: :muted)
|
||||||
|
@steps << { type: :say, text: text, style: style }
|
||||||
|
end
|
||||||
|
|
||||||
|
def set(name, value)
|
||||||
|
@steps << { type: :set, name: name, value: value }
|
||||||
|
end
|
||||||
|
|
||||||
|
def ask(name, prompt:, transform: nil, validate: nil)
|
||||||
|
@steps << { type: :ask, name: name, prompt: prompt,
|
||||||
|
transform: transform, validate: validate }
|
||||||
|
end
|
||||||
|
|
||||||
|
def persist(*fields, store:)
|
||||||
|
@steps << { type: :persist, fields: fields.flatten, store: store }
|
||||||
|
end
|
||||||
|
|
||||||
|
def pause(message, seconds: 1.0)
|
||||||
|
@steps << { type: :pause, message: message, seconds: seconds }
|
||||||
|
end
|
||||||
|
|
||||||
|
def gate(denied:, &check)
|
||||||
|
@steps << { type: :gate, check: check, denied: denied }
|
||||||
|
end
|
||||||
|
|
||||||
|
def confirm(message, denied: nil, &block)
|
||||||
|
step = { message: message, denied: denied }
|
||||||
|
if block_given?
|
||||||
|
sub = Flow.new
|
||||||
|
sub.instance_eval(&block)
|
||||||
|
@steps << step.merge(type: :confirm_block, sub_steps: sub.steps)
|
||||||
|
else
|
||||||
|
@steps << step.merge(type: :confirm_halt)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def menu(prompt, loop: false, &block)
|
||||||
|
builder = MenuBuilder.new
|
||||||
|
builder.instance_eval(&block)
|
||||||
|
@steps << { type: :menu, prompt: prompt, loop: loop, options: builder.options }
|
||||||
|
end
|
||||||
|
|
||||||
|
def exit_menu
|
||||||
|
@steps << { type: :exit_menu }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class MenuBuilder
|
||||||
|
attr_reader :options
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@options = []
|
||||||
|
end
|
||||||
|
|
||||||
|
def option(label, &block)
|
||||||
|
sub = Flow.new
|
||||||
|
sub.instance_eval(&block) if block_given?
|
||||||
|
@options << { label: label, steps: sub.steps }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
190
lib/bbs/flow_runner.rb
Normal file
190
lib/bbs/flow_runner.rb
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module BBS
|
||||||
|
class FlowRunner
|
||||||
|
STYLES = {
|
||||||
|
success: "\e[1;32m",
|
||||||
|
muted: "\e[0;37m",
|
||||||
|
error: "\e[1;31m",
|
||||||
|
info: "\e[0;33m",
|
||||||
|
prompt: "\e[1;37m",
|
||||||
|
confirm: "\e[1;36m",
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def initialize(session, session_id, flow)
|
||||||
|
@session = session
|
||||||
|
@session_id = session_id
|
||||||
|
@flow = flow
|
||||||
|
@ctx = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def run
|
||||||
|
execute(@flow.steps)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def execute(steps)
|
||||||
|
steps.each do |step|
|
||||||
|
result = dispatch(step)
|
||||||
|
return result if result == :halt || result == :exit_menu
|
||||||
|
end
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def dispatch(step)
|
||||||
|
case step[:type]
|
||||||
|
when :screen then run_screen(step)
|
||||||
|
when :banner then run_banner(step)
|
||||||
|
when :big_banner then run_big_banner(step)
|
||||||
|
when :say then run_say(step)
|
||||||
|
when :set then run_set(step)
|
||||||
|
when :ask then run_ask(step)
|
||||||
|
when :persist then run_persist(step)
|
||||||
|
when :pause then run_pause(step)
|
||||||
|
when :gate then run_gate(step)
|
||||||
|
when :confirm_halt then run_confirm_halt(step)
|
||||||
|
when :confirm_block then run_confirm_block(step)
|
||||||
|
when :menu then run_menu(step)
|
||||||
|
when :exit_menu then :exit_menu
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── step handlers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def run_screen(step)
|
||||||
|
vars = step[:vars].transform_values { |v| v.is_a?(Symbol) ? @ctx[v] : v }
|
||||||
|
write Renderer.render(step[:name].to_s, @ctx.merge(vars))
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_banner(step)
|
||||||
|
color = STYLES.fetch(step[:style], STYLES[:success])
|
||||||
|
write Banner.render(step[:text], color: color)
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_big_banner(step)
|
||||||
|
color = STYLES.fetch(step[:style], STYLES[:success])
|
||||||
|
write Banner.render_big(step[:text], color: color, font: step[:font])
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_say(step)
|
||||||
|
color = STYLES.fetch(step[:style], STYLES[:muted])
|
||||||
|
write "\r\n #{color}#{step[:text]}\e[0m\r\n\r\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_set(step)
|
||||||
|
@ctx[step[:name]] = step[:value]
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_ask(step)
|
||||||
|
write " #{STYLES[:prompt]}#{step[:prompt]}\e[0m: "
|
||||||
|
value = readline&.strip
|
||||||
|
return :halt if value.nil?
|
||||||
|
|
||||||
|
value = apply_transform(value, step[:transform])
|
||||||
|
|
||||||
|
if step[:validate] && !valid?(value, step[:validate])
|
||||||
|
write "\r\n #{STYLES[:error]}#{validation_message(step[:validate])}\e[0m\r\n\r\n"
|
||||||
|
return :halt
|
||||||
|
end
|
||||||
|
|
||||||
|
@ctx[step[:name]] = value
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_persist(step)
|
||||||
|
fields = step[:fields].filter_map { |f| [f, @ctx[f]] if @ctx.key?(f) }.to_h
|
||||||
|
step[:store].upsert(session_id: @session_id, **fields) unless fields.empty?
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_pause(step)
|
||||||
|
write "\r\n #{STYLES[:info]}#{step[:message]}\e[0m\r\n"
|
||||||
|
sleep step[:seconds]
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_gate(step)
|
||||||
|
return nil if step[:check].call(@ctx)
|
||||||
|
write " #{STYLES[:error]}#{step[:denied]}\e[0m\r\n\r\n"
|
||||||
|
:halt
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_confirm_halt(step)
|
||||||
|
write "\r\n #{STYLES[:confirm]}#{step[:message]}\e[0m [yes/no]: "
|
||||||
|
answer = readline&.strip&.downcase
|
||||||
|
return :halt if answer.nil?
|
||||||
|
return nil if %w[yes y].include?(answer)
|
||||||
|
write "\r\n #{STYLES[:muted]}#{step[:denied]}\e[0m\r\n\r\n" if step[:denied]
|
||||||
|
:halt
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_confirm_block(step)
|
||||||
|
write "\r\n #{STYLES[:prompt]}#{step[:message]}\e[0m [yes/no]: "
|
||||||
|
answer = readline&.strip&.downcase
|
||||||
|
return :halt if answer.nil?
|
||||||
|
|
||||||
|
if %w[yes y].include?(answer)
|
||||||
|
execute(step[:sub_steps])
|
||||||
|
else
|
||||||
|
write "\r\n #{STYLES[:muted]}#{step[:denied]}\e[0m\r\n\r\n" if step[:denied]
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_menu(step)
|
||||||
|
loop do
|
||||||
|
write "\r\n"
|
||||||
|
step[:options].each_with_index do |opt, i|
|
||||||
|
write " #{STYLES[:muted]}#{i + 1}. #{opt[:label]}\e[0m\r\n"
|
||||||
|
end
|
||||||
|
write "\r\n #{STYLES[:prompt]}#{step[:prompt]}\e[0m "
|
||||||
|
|
||||||
|
input = readline&.strip
|
||||||
|
return :halt if input.nil?
|
||||||
|
|
||||||
|
index = input.to_i - 1
|
||||||
|
unless (0...step[:options].length).include?(index)
|
||||||
|
write "\r\n #{STYLES[:error]}Enter a number between 1 and #{step[:options].length}.\e[0m\r\n"
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
result = execute(step[:options][index][:steps])
|
||||||
|
return :halt if result == :halt
|
||||||
|
return nil if result == :exit_menu
|
||||||
|
|
||||||
|
break unless step[:loop]
|
||||||
|
end
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── helpers ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def apply_transform(value, transform)
|
||||||
|
case transform
|
||||||
|
when :upcase then value.upcase
|
||||||
|
when :downcase then value.downcase
|
||||||
|
else value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid?(value, validator)
|
||||||
|
case validator
|
||||||
|
when :email then value.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/)
|
||||||
|
when :non_empty then !value.empty?
|
||||||
|
else true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validation_message(validator)
|
||||||
|
case validator
|
||||||
|
when :email then "That doesn't look like a valid email address. Farewell."
|
||||||
|
else "Invalid input. Farewell."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def write(data) = @session.write(data)
|
||||||
|
def readline = @session.readline
|
||||||
|
end
|
||||||
|
end
|
||||||
48
lib/bbs/renderer.rb
Normal file
48
lib/bbs/renderer.rb
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'erb'
|
||||||
|
require 'artii'
|
||||||
|
|
||||||
|
module BBS
|
||||||
|
class Renderer
|
||||||
|
def self.render(name, vars = {})
|
||||||
|
dir = BBS.config.screens_dir or raise "BBS.config.screens_dir is not set"
|
||||||
|
path = File.join(dir, "#{name}.erb")
|
||||||
|
ctx = Context.new(vars)
|
||||||
|
ERB.new(File.read(path), trim_mode: '-').result(ctx.ctx_binding).gsub("\n", "\r\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
class Context
|
||||||
|
def initialize(vars)
|
||||||
|
vars.each { |k, v| instance_variable_set(:"@#{k}", v) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def ctx_binding = binding
|
||||||
|
|
||||||
|
def banner(text, padding: 2)
|
||||||
|
inner = "#{' ' * padding}#{text}#{' ' * padding}"
|
||||||
|
bar = '═' * inner.length
|
||||||
|
"#{green}╔#{bar}╗#{reset}\n" \
|
||||||
|
"#{green}║#{inner}║#{reset}\n" \
|
||||||
|
"#{green}╚#{bar}╝#{reset}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def big_banner(text, font: 'slant')
|
||||||
|
Artii::Base.new(font: font).asciify(text)
|
||||||
|
.split("\n")
|
||||||
|
.map { |l| " #{green}#{l}#{reset}" }
|
||||||
|
.join("\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
def clr = "\e[2J\e[H"
|
||||||
|
def reset = "\e[0m"
|
||||||
|
def green = "\e[1;32m"
|
||||||
|
def yellow = "\e[1;33m"
|
||||||
|
def cyan = "\e[1;36m"
|
||||||
|
def white = "\e[1;37m"
|
||||||
|
def gray = "\e[0;37m"
|
||||||
|
def red = "\e[1;31m"
|
||||||
|
def dim_green = "\e[0;32m"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
22
lib/bbs/server.rb
Normal file
22
lib/bbs/server.rb
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'socket'
|
||||||
|
|
||||||
|
module BBS
|
||||||
|
class Server
|
||||||
|
def initialize(port:)
|
||||||
|
@port = port
|
||||||
|
end
|
||||||
|
|
||||||
|
def run
|
||||||
|
server = TCPServer.new('0.0.0.0', @port)
|
||||||
|
puts "BBS listening on port #{@port} — connect with: telnet localhost #{@port}"
|
||||||
|
loop do
|
||||||
|
client = server.accept
|
||||||
|
Thread.new(client) { |c| Session.new(c).run rescue nil }
|
||||||
|
end
|
||||||
|
rescue Interrupt
|
||||||
|
puts "\nServer stopped."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
22
lib/bbs/session.rb
Normal file
22
lib/bbs/session.rb
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'securerandom'
|
||||||
|
|
||||||
|
module BBS
|
||||||
|
class Session
|
||||||
|
include Telnet
|
||||||
|
|
||||||
|
def initialize(client)
|
||||||
|
@client = client
|
||||||
|
@session_id = SecureRandom.hex(8)
|
||||||
|
end
|
||||||
|
|
||||||
|
def run
|
||||||
|
negotiate
|
||||||
|
flow = BBS.config.flow or raise "BBS.config.flow is not set"
|
||||||
|
FlowRunner.new(self, @session_id, flow).run
|
||||||
|
ensure
|
||||||
|
@client.close rescue nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
49
lib/bbs/store.rb
Normal file
49
lib/bbs/store.rb
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'csv'
|
||||||
|
require 'fileutils'
|
||||||
|
require 'time'
|
||||||
|
|
||||||
|
module BBS
|
||||||
|
class Store
|
||||||
|
def initialize(path:, headers:)
|
||||||
|
@path = path
|
||||||
|
@headers = headers
|
||||||
|
@mutex = Mutex.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def upsert(session_id:, **fields)
|
||||||
|
@mutex.synchronize do
|
||||||
|
ensure_file!
|
||||||
|
rows = CSV.read(@path, headers: true)
|
||||||
|
row = rows.find { |r| r['session_id'] == session_id }
|
||||||
|
|
||||||
|
if row
|
||||||
|
fields.each { |k, v| row[k.to_s] = v }
|
||||||
|
else
|
||||||
|
values = @headers.map do |h|
|
||||||
|
case h
|
||||||
|
when 'session_id' then session_id
|
||||||
|
when 'timestamp' then Time.now.utc.iso8601
|
||||||
|
else fields[h.to_sym]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rows << CSV::Row.new(@headers, values)
|
||||||
|
end
|
||||||
|
|
||||||
|
CSV.open(@path, 'w') do |csv|
|
||||||
|
csv << @headers
|
||||||
|
rows.each { |r| csv << r.fields }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ensure_file!
|
||||||
|
FileUtils.mkdir_p(File.dirname(@path))
|
||||||
|
return if File.exist?(@path)
|
||||||
|
CSV.open(@path, 'w') { |csv| csv << @headers }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
98
lib/bbs/telnet.rb
Normal file
98
lib/bbs/telnet.rb
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module BBS
|
||||||
|
module Telnet
|
||||||
|
IAC = 255
|
||||||
|
WILL = 251
|
||||||
|
WONT = 252
|
||||||
|
DO = 253
|
||||||
|
DONT = 254
|
||||||
|
SB = 250
|
||||||
|
SE = 240
|
||||||
|
|
||||||
|
ECHO_OPT = 1
|
||||||
|
SGA_OPT = 3
|
||||||
|
|
||||||
|
def negotiate
|
||||||
|
send_raw [IAC, WILL, SGA_OPT].pack('C*')
|
||||||
|
send_raw [IAC, WILL, ECHO_OPT].pack('C*')
|
||||||
|
send_raw [IAC, DO, SGA_OPT].pack('C*')
|
||||||
|
end
|
||||||
|
|
||||||
|
def readline
|
||||||
|
line = +""
|
||||||
|
last_cr = false
|
||||||
|
|
||||||
|
loop do
|
||||||
|
raw = @client.read(1)
|
||||||
|
return nil if raw.nil?
|
||||||
|
byte = raw.ord
|
||||||
|
|
||||||
|
if byte == IAC
|
||||||
|
absorb_iac
|
||||||
|
last_cr = false
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
if byte == 13
|
||||||
|
last_cr = true
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
if byte == 10 || (byte == 0 && last_cr)
|
||||||
|
write "\r\n"
|
||||||
|
return line
|
||||||
|
end
|
||||||
|
|
||||||
|
last_cr = false
|
||||||
|
next if byte == 0
|
||||||
|
|
||||||
|
if byte == 8 || byte == 127
|
||||||
|
unless line.empty?
|
||||||
|
line.chop!
|
||||||
|
write "\b \b"
|
||||||
|
end
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
next if byte < 32
|
||||||
|
|
||||||
|
line << raw
|
||||||
|
write raw
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def write(data)
|
||||||
|
@client.write(data)
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def absorb_iac
|
||||||
|
cmd = @client.read(1)
|
||||||
|
return if cmd.nil?
|
||||||
|
|
||||||
|
case cmd.ord
|
||||||
|
when SB
|
||||||
|
loop do
|
||||||
|
b = @client.read(1)
|
||||||
|
break if b.nil?
|
||||||
|
if b.ord == IAC
|
||||||
|
s = @client.read(1)
|
||||||
|
break if s.nil? || s.ord == SE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
when WILL, WONT, DO, DONT
|
||||||
|
@client.read(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_raw(data)
|
||||||
|
@client.write(data)
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user