root/lang/ruby/partty/trunk/parttyd.rb @ 8625

Revision 8625, 6.5 kB (checked in by frsyuki, 5 years ago)

lang/ruby/partty: added Ruby Partty! server

Line 
1require 'rubygems'
2require 'eventmachine'
3require 'emtelnet'
4
5
6class ParttyServer
7        def initialize
8                @hosts = {}
9        end
10        def add_host(session, host)
11                puts "Start session #{session.inspect}"
12                @hosts[session] = host
13        end
14        def remove_host(session)
15                puts "Close session #{session.inspect}"
16                @hosts.delete(session)
17        end
18        def [](sessoin)
19                @hosts[sessoin]
20        end
21end
22
23Server = ParttyServer.new
24
25
26
27module TelnetStateIO
28
29        def initialize
30                @telnet = Emtelnet.new
31        end
32
33        def receive_data data
34                inbuf, outbuf = @telnet.recv(data)
35                unless outbuf.empty?
36                        send_data outbuf
37                end
38                unless inbuf.empty?
39                        __send__ @state, inbuf
40                end
41        end
42
43protected
44        def send(data)
45                send_data @telnet.send(data)
46        end
47
48        def set_state(state)
49                @state = state
50        end
51
52        LINE_BREAK_CHRSETSET = /[^#{Regexp.escape('!#$%\'()*+-.:;=?@[\]_`{}~ ')}\w]/
53        def recv_line(data, buf, max)
54                unless (npos = data.index(LINE_BREAK_CHRSETSET)).nil?
55                        data.slice!(npos..-1)
56                        buf << data
57                        buf.slice!(max..-1)
58                        return true
59                else
60                        buf << data
61                        if buf.length > max
62                                buf.slice!(max..-1)
63                                return true
64                        end
65                        return false
66                end
67        end
68
69        def recv_fixed(data, buf, len)
70                buf << data
71                if buf.length > len
72                        rest = buf.slice!(len..-1)
73                        rest
74                elsif buf.length == len
75                        ""
76                else
77                        nil
78                end
79        end
80end
81
82
83module Host
84        include TelnetStateIO
85
86        def initialize
87                super
88                @guests = []
89                @session = ""
90                @write_pass = ""
91                @read_pass = ""
92                @header = ""
93                @header_data = ""
94                @host = nil
95        end
96
97        def post_init
98                set_state :recv_init
99        end
100
101        def unbind
102                if @host
103                        Server.remove_host(@session)
104                end
105        end
106
107        NEGOTIATION_MAGIC_STRING = "Partty!"
108        NEGOTIATION_HEADER_LENGTH = 7 + 1 + 2*4
109        module NEGOTIATION_REPLY
110                SUCCESS = 0
111                PROTOCOL_MISMATCH = 1
112                SESSION_UNAVAILABLE = 2
113                SERVER_ERROR = 3
114                AUTHENTICATION_FAILED = 4
115        end
116
117        def recv_init(data)
118                return unless rest_data = recv_fixed(data, @header, NEGOTIATION_HEADER_LENGTH)
119                magic, protocol_version, @message_len, @session_len, @wpass_len, @rpass_len =
120                        @header.unpack("a7cn4")
121                if magic != NEGOTIATION_MAGIC_STRING
122                        send_negotiation_reply(NEGOTIATION_REPLY::PROTOCOL_MISMATCH, "protocol mismatch")
123                        return close_connection_after_writing
124                end
125
126                # FIXME check protocol_version
127
128                set_state :recv_header
129                recv_header(rest_data)
130        end
131
132        def recv_header(data)
133                return unless recv_fixed(data, @header_data, @message_len + @session_len + @wpass_len + @rpass_len)
134                @message, @session, @write_pass, @read_pass =
135                        @header_data.unpack("a#{@message_len}a#{@session_len}a#{@wpass_len}a#{@rpass_len}")
136                send_negotiation_reply(NEGOTIATION_REPLY::SUCCESS, "ok")
137                @host = Server.add_host(@session, self)
138                set_state :recv_term
139        end
140
141        def recv_term(data)
142                @guests.each {|g|
143                        g.send data
144                }
145        end
146
147        def guest_data(guest, data)
148                if guest.info.writable?
149                        send data
150                end
151        end
152
153
154        class GuestInfo
155                def initialize(w)
156                        @writable = w
157                end
158                attr_accessor :writable
159                def writable?
160                        @writable
161                end
162        end
163
164        def add_guest(guest, password)
165                if password == @write_pass
166                        puts "#{@session.inspect}: accepted operatable guest #{guest.inspect}"
167                        @guests << guest
168                        GuestInfo.new(true)
169                elsif password == @read_pass
170                        puts "#{@session.inspect}: accepted view-only guest #{guest.inspect}"
171                        @guests << guest
172                        GuestInfo.new(false)
173                else
174                        nil
175                end
176        end
177
178        def remove_guest(guest)
179                puts "#{@session.inspect}: remove guest #{guest.inspect}"
180                @guests.delete(guest)
181        end
182
183private
184        def send_negotiation_reply(code, msg)
185                send_data [code, msg.length].pack("n2") + msg
186        end
187end
188
189
190module Guest
191        include TelnetStateIO
192
193        def initialize
194                super
195                @session = ""
196                @password = ""
197                @host = nil
198                @info = nil
199
200                # telnet option: force clinet line mode
201
202                # use these options
203                @telnet.my_option_handler[Emtelnet::OPT_BINARY] = Proc.new {}
204
205                # supported partner options
206                @telnet.partner_option_handler[Emtelnet::OPT_ECHO] = Proc.new {}
207                @telnet.partner_option_handler[Emtelnet::OPT_LINEMODE] = Proc.new {}
208                @telnet.partner_option_handler[Emtelnet::OPT_BINARY] = Proc.new {}
209        end
210
211        attr_reader :info
212
213
214        def session_init
215                # add these options
216                @telnet.my_option_handler[Emtelnet::OPT_SGA] = Proc.new {}
217                @telnet.my_option_handler[Emtelnet::OPT_NAWS] = Proc.new {}
218
219                # add supported partner options
220                @telnet.partner_option_handler[Emtelnet::OPT_BINARY] = Proc.new {}
221
222                # prevent line mode
223                @telnet.send_will(Emtelnet::OPT_SGA);
224                @telnet.send_will(Emtelnet::OPT_ECHO);
225                @telnet.send_dont(Emtelnet::OPT_ECHO);
226                @telnet.send_dont(Emtelnet::OPT_LINEMODE);
227
228                # enable multibyte characters
229                @telnet.send_will(Emtelnet::OPT_BINARY);
230                @telnet.send_do(Emtelnet::OPT_BINARY);
231
232                # window size
233                @telnet.send_will(Emtelnet::OPT_NAWS);
234        end
235        private :session_init
236
237
238        # called when the connection is closed
239        def unbind
240                reset_host
241        end
242
243        def post_init
244                send "Session name: "
245                set_state :recv_session_name
246        end
247
248        def recv_session_name(data)
249                if recv_line(data, @session, 64)
250                        # prevent local-echo
251                        @telnet.send_will(Emtelnet::OPT_ECHO)
252                        @telnet.send_dont(Emtelnet::OPT_ECHO)
253
254                        send "Password: "
255                        @state = :recv_password
256                end
257        end
258
259        def recv_password(data)
260                if recv_line(data, @password, 64)
261                        unless err = set_host
262                                send "\r\nPartty! connected.\r\n"
263                                @state = :recv_operation
264                        else
265                                send "\r\n#{err}.\r\n"
266                                close_connection_after_writing
267                        end
268                end
269        end
270
271        def recv_operation(data)
272                @host.guest_data(self, data)
273        end
274
275private
276
277        def set_host
278                host = Server[@session]
279                return "No such session `#{@session}'" unless host
280                if @info = host.add_guest(self, @password)
281                        @host = host
282                        session_init
283                        nil
284                else
285                        return "Password not match"
286                end
287        end
288
289        def reset_host
290                if @host
291                        @host.remove_guest(self)
292                end
293        end
294
295end
296
297
298
299require 'optparse'
300op = OptionParser.new
301
302op.version = "0.4.0"
303op.banner = "Partty! Server #{op.version}\n#{op.banner}"
304
305options = {
306        :host_port => 10000,
307        :guest_port => 20000,
308        :flash_guest_port => nil,
309        :listen_addr => "0.0.0.0",
310}
311
312op.on('-l <port>', "listen port for partty-host [#{options[:host_port]}]", Integer) {|arg|
313        options[:host_port] = arg
314}
315
316op.on('-g <port>', "listen port for telnet guest [#{options[:guest_port]}]", Integer) {|arg|
317        options[:guest_port] = arg
318}
319
320op.on('-f <port>', "listen port for flash telnet guest [don't listen]", Integer) {|arg|
321        options[:flash_guest_port] = arg
322}
323
324op.on('-a <address>', "listen address [#{options[:listen_addr]}]", String) {|arg|
325        options[:listen_addr] = arg
326}
327
328op.parse!(ARGV)
329unless ARGV.empty?
330        puts op.ver
331        puts op.help
332        exit
333end
334
335EventMachine::run do
336        EventMachine.start_server options[:listen_addr], options[:host_port], Host
337        EventMachine.start_server options[:listen_addr], options[:guest_port], Guest
338end
339
340
Note: See TracBrowser for help on using the browser.