webricknio 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,499 @@
1
+ #
2
+ # httpserver.rb -- HTTPServer Class
3
+ #
4
+ # Author: IPR -- Internet Programming with Ruby -- writers, Pradeep Singh
5
+ # Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
6
+ # Copyright (c) 2002 Internet Programming with Ruby writers
7
+ # Copyright (c) 2012 Pradeep Singh
8
+ # All rights reserved.
9
+
10
+ require 'socket'
11
+ require 'webricknio/config'
12
+ require 'webrick/log'
13
+ require 'webricknio/log'
14
+ require 'webricknio/httprequest'
15
+ require 'webricknio/httpresponse'
16
+ require 'webrick/server'
17
+
18
+ require 'webricknio/accesslog'
19
+
20
+ require 'webricknio/block'
21
+
22
+ require 'set'
23
+
24
+ require 'java'
25
+
26
+ java_import 'java.nio.ByteBuffer'
27
+
28
+ java_import 'java.nio.channels.ServerSocketChannel'
29
+ java_import 'java.nio.channels.Selector'
30
+ java_import 'java.nio.channels.SelectionKey'
31
+
32
+ java_import 'java.net.ServerSocket'
33
+ java_import 'java.net.InetSocketAddress'
34
+
35
+ java_import 'java.util.Iterator'
36
+
37
+ module WEBrickNIO
38
+
39
+ class HTTPServer
40
+ attr_reader :status, :config, :logger, :selector
41
+
42
+ def initialize(config={}, default=Config::HTTP)
43
+
44
+ begin
45
+ file_name = "#{Rails.root}/config/webricknio.rb"
46
+ file = File.open(file_name, 'r')
47
+ hash = eval file.read
48
+ @config = default.update(hash).update(config)
49
+ rescue
50
+ @config = default.dup.update(config)
51
+ puts "custom config file not present"
52
+ end
53
+
54
+ @config[:Logger] ||= ::WEBrickNIO::Log::new @config[:LogLocation]
55
+ @logger = @config[:Logger]
56
+ @logger.level = @config[:LogLevel] if @config[:LogLevel]
57
+
58
+ @logger.info "configured properties:\n#{@config}"
59
+
60
+ @status = :Stop
61
+
62
+ poolv = WEBrickNIO::VERSION
63
+ rubyv = "#{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
64
+ @logger.info("HTTPServerNIO #{poolv}")
65
+ @logger.info("ruby #{rubyv}")
66
+
67
+ @mount_tab = MountTable.new
68
+
69
+ unless @config[:AccessLog]
70
+ @config[:AccessLog] = [
71
+ [ $stderr, ::WEBrickNIO::AccessLog::COMMON_LOG_FORMAT ],
72
+ [ $stderr, ::WEBrickNIO::AccessLog::REFERER_LOG_FORMAT ]
73
+ ]
74
+ end
75
+
76
+ @blocker_chain = WEBrickNIO::ChainedBlock.new
77
+
78
+ trap(:INT) do
79
+ @logger.info "SIGINT in WEBrickNIO::HTTPServer"
80
+ shutdown
81
+ end
82
+
83
+ trap(:HUP) do
84
+ @logger.info "SIGHUP in WEBrickNIO::HTTPServer"
85
+ reload
86
+ end
87
+ end
88
+
89
+ def reload
90
+ @blocker_chain.reload
91
+ end
92
+
93
+
94
+ def start(&block)
95
+ raise ::WEBrick::ServerError, "already started." if @status != :Stop
96
+ @logger.info \
97
+ "#{self.class}#start: pid=#{$$} port=#{@config[:Port]}"
98
+
99
+ @server_socket_channel = ServerSocketChannel.open
100
+ @server_socket_channel.configure_blocking false
101
+
102
+ sock_addr = InetSocketAddress.new(@config[:Port])
103
+ server_sock = @server_socket_channel.socket.bind sock_addr
104
+ @logger.info "bound socket #{@server_socket_channel.socket.object_id}"
105
+
106
+ @selector = Selector.open
107
+ @main_selector_key = @server_socket_channel.register(@selector, SelectionKey::OP_ACCEPT);
108
+ @logger.info "registered selector"
109
+
110
+ @thread_pool = java.util.concurrent.Executors.newFixedThreadPool(@config[:NumThreads])
111
+ @logger.info "creating thread pool of size #{@config[:NumThreads]}"
112
+
113
+ @status = :Running
114
+ while @status == :Running
115
+
116
+ begin
117
+ @selector.select # go ahead and block here forever
118
+ ready_keys = @selector.selected_keys
119
+ rescue Exception => ex
120
+ if @status == :Running
121
+ @logger.error "SELECTOR exception: #{ex.java_class.name}"
122
+ #ex.print_stack_trace
123
+ else
124
+ @logger.info "selector shutdown"
125
+ end
126
+ next
127
+ end
128
+
129
+ begin
130
+ iterator = ready_keys.iterator
131
+
132
+ while iterator.has_next
133
+ key = iterator.next
134
+ iterator.remove
135
+
136
+ if key.is_valid && key.is_acceptable
137
+ client_channel = @server_socket_channel.accept
138
+ client_channel.configure_blocking false
139
+ sock = client_channel.socket
140
+ remote_addr = sock.getInetAddress
141
+ @logger.info "accepted connection from: #{remote_addr.getHostAddress}"
142
+ if @blocker_chain.block_ip? remote_addr.getHostAddress
143
+ @logger.info "blocked ip: #{remote_addr.getHostAddress}"
144
+ client_channel.close
145
+ else
146
+ key2 = client_channel.register(@selector, SelectionKey::OP_READ, Attachment.new)
147
+ end
148
+ elsif key.is_valid && key.is_readable
149
+ unless key.attachment.is_locked?
150
+ key.attachment.lock
151
+ sock = key.channel.socket
152
+ sock_channel = key.channel
153
+ @thread_pool.submit key.attachment.handler.new(key, self, @config, key.attachment)
154
+ end
155
+ else
156
+ key.cancel
157
+ end
158
+
159
+ end # while iterator
160
+
161
+ rescue Exception => ex # to get rid of canceled key exception
162
+ if ex.is_a?(Exception)
163
+ @logger.error(ex)
164
+ else
165
+ message = ""
166
+ if ex.respond_to?(:java_class) && ex.respond_to?(:stack_trace)
167
+ #ex.print_stack_trace
168
+ message += ex.java_class.name + ": " unless ex.java_class.name.nil?
169
+ else
170
+ message = ex.inspect
171
+ end
172
+ @logger.error(message)
173
+ end
174
+ end
175
+
176
+ end # while Running
177
+
178
+ @logger.info "going to shutdown ..."
179
+ @logger.info "#{self.class}#start done."
180
+ @status = :Stop
181
+ shutdown if @status != :Shutdown
182
+ end
183
+
184
+ #
185
+ # Maintains state information for a socket while processing requests.
186
+ # #cleanup needs to be called after every finished processing of a request for a socket
187
+ #
188
+
189
+ class Attachment
190
+ attr_accessor :request, :response, :handler
191
+
192
+ def initialize
193
+ cleanup
194
+ end
195
+
196
+ def lock
197
+ @locked = true
198
+ end
199
+
200
+ def unlock
201
+ @locked = false
202
+ end
203
+
204
+ def is_locked?
205
+ @locked
206
+ end
207
+
208
+ def cleanup
209
+
210
+ # This is not a synchronization lock.
211
+ # It is there only to block the flood of signals generated when an event occurs.
212
+ @locked = false
213
+
214
+ # Handler that will process requests coming on this socket.
215
+ # Default is RequestHandler which assumes a regular HTTP request. In some cases, such as websockets, handler can be changed
216
+ # to some other class after initial negotiation has been completed using the default handler.
217
+ @handler = RequestHandler
218
+
219
+ @request = nil
220
+ @response = nil
221
+ end
222
+
223
+ end
224
+
225
+
226
+ #
227
+ # Instances of this class are submitted to the threadpool to process incoming requests.
228
+ # It will very likely process the whole request in one shot but it might postpone the request too for a slow client
229
+ #
230
+
231
+ class RequestHandler
232
+ include java.util.concurrent.Callable
233
+
234
+ def initialize(key, server, config, attachment)
235
+ @key = key
236
+ @sock_channel = @key.channel
237
+ @config = config
238
+ @server = server
239
+ @send_response = true
240
+ @socket_id = -1
241
+ @attachment = attachment
242
+ end
243
+
244
+ def call
245
+ begin
246
+ time1 = Time.now
247
+ req = @attachment.request || ::WEBrickNIO::HTTPRequest.new(@config)
248
+ res = @attachment.response || ::WEBrickNIO::HTTPResponse.new(@config)
249
+
250
+ @socket_id = @sock_channel.socket.object_id
251
+
252
+ socket = @sock_channel.socket
253
+
254
+ if req.in_progress?
255
+ @server.logger.debug "resuming request in progress"
256
+ req.resume
257
+ else
258
+ # fresh request
259
+ req.parse(@sock_channel)
260
+ end
261
+
262
+ if req.in_progress?
263
+ @server.logger.debug "request in progress"
264
+ @attachment.request = req if @attachment.request.nil?
265
+ @attachment.response = res if @attachment.response.nil?
266
+ @send_response = false
267
+ return # goes to "ensure" first
268
+ end
269
+
270
+ res.request_method = req.request_method
271
+ res.request_uri = req.request_uri
272
+ res.request_http_version = req.http_version
273
+ res.keep_alive = req.keep_alive?
274
+
275
+ if req.unparsed_uri == "*"
276
+ if req.request_method == "OPTIONS"
277
+ do_OPTIONS(req, res)
278
+ raise ::WEBrick::HTTPStatus::OK
279
+ end
280
+ raise ::WEBrick::HTTPStatus::NotFound, "`#{req.unparsed_uri}' not found."
281
+ end
282
+
283
+ servlet, options, script_name, path_info = @server.search_servlet(req.path)
284
+ raise ::WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found." unless servlet
285
+ req.script_name = script_name
286
+ req.path_info = path_info
287
+ si = servlet.get_instance(self, *options)
288
+ #@server.logger.debug(format("%s is invoked.", si.class.name))
289
+ @server.access_log(@config, req, res)
290
+
291
+ si.service(req, res)
292
+
293
+ rescue ::WEBrick::HTTPStatus::EOFError => ex
294
+ @send_response = false
295
+ @attachment.cleanup
296
+ @server.logger.debug(ex.message)
297
+ rescue ::WEBrick::HTTPStatus::Error => ex
298
+ @server.logger.error(ex)
299
+ res.set_error(ex)
300
+ rescue ::WEBrick::HTTPStatus::Status => ex
301
+ res.status = ex.code
302
+ rescue StandardError => ex
303
+ @server.logger.error(ex)
304
+ res.set_error(ex)
305
+ rescue Exception => ex
306
+ if ex.is_a?(Exception)
307
+ @server.logger.error(ex)
308
+ else
309
+ message = ""
310
+ if ex.respond_to?(:java_class) && ex.respond_to?(:stack_trace)
311
+ @server.logger.error("error")
312
+ ex.print_stack_trace
313
+ else
314
+ message = ex.inspect
315
+ end
316
+ @server.logger.error(message)
317
+ end
318
+ res.set_error("500")
319
+ ensure
320
+ begin
321
+ time3 = Time.now
322
+ @attachment.unlock
323
+ if @send_response
324
+ @attachment.cleanup
325
+ res.send_response(@sock_channel)
326
+ @server.logger.debug "time taken to send response #{Time.now - time3}"
327
+ end
328
+ if (!req.keep_alive? || !res.keep_alive? || (!@send_response && !req.in_progress?))
329
+ @server.logger.debug("closing socket. req.keep alive: #{req.keep_alive}, resp.keep alive: #{res.keep_alive}, send_response: #{@send_response}, socket id: #{@socket_id}")
330
+ @sock_channel.close
331
+ @key.cancel
332
+ end
333
+ rescue Exception => ex
334
+ if ex.is_a?(Exception)
335
+ @server.logger.error(ex)
336
+ else
337
+ message = ""
338
+ if ex.respond_to?(:java_class) && ex.respond_to?(:stack_trace)
339
+ @server.logger.error("error")
340
+ ex.print_stack_trace
341
+ else
342
+ message = ex.inspect
343
+ end
344
+ @server.logger.error(message)
345
+ end
346
+ end
347
+ time2 = Time.now
348
+ @server.logger.info "total request time: #{time2 - time1} sec" if @send_response
349
+ end
350
+ end
351
+
352
+ end
353
+
354
+ #
355
+ # TBD
356
+ #
357
+
358
+ class WebsocketHandler
359
+ include java.util.concurrent.Callable
360
+
361
+ def initialize(key, server, config, attachment)
362
+ @key = key
363
+ @sock_channel = @key.channel
364
+ @config = config
365
+ @server = server
366
+ @send_response = true
367
+ @socket_id = -1
368
+ @attachment = attachment
369
+ end
370
+
371
+ def call
372
+ end
373
+
374
+ end
375
+
376
+
377
+ def stop
378
+ if @status == :Running
379
+ @status = :Shutdown
380
+ end
381
+ end
382
+
383
+ def shutdown
384
+ stop
385
+ @server_socket_channel.close
386
+ @selector.close
387
+ @thread_pool.shutdown
388
+ @logger.debug "shutdown thread pool"
389
+ end
390
+
391
+ def [](key)
392
+ @config[key]
393
+ end
394
+
395
+
396
+
397
+ def do_OPTIONS(req, res)
398
+ res["allow"] = "GET,HEAD,POST,OPTIONS"
399
+ end
400
+
401
+ ##
402
+ # Mounts +servlet+ on +dir+ passing +options+ to the servlet at creation
403
+ # time
404
+
405
+ def mount(dir, servlet, *options)
406
+ @logger.debug(sprintf("%s is mounted on %s", servlet.inspect, dir))
407
+ @mount_tab[dir] = [ servlet, options ]
408
+ end
409
+
410
+ ##
411
+ # Mounts +proc+ or +block+ on +dir+ and calls it with a
412
+ # WEBrick::HTTPRequest and WEBrick::HTTPResponse
413
+
414
+ def mount_proc(dir, proc=nil, &block)
415
+ proc ||= block
416
+ raise HTTPServerError, "must pass a proc or block" unless proc
417
+ mount(dir, HTTPServlet::ProcHandler.new(proc))
418
+ end
419
+
420
+ ##
421
+ # Unmounts +dir+
422
+
423
+ def unmount(dir)
424
+ @logger.debug(sprintf("unmount %s.", dir))
425
+ @mount_tab.delete(dir)
426
+ end
427
+ alias umount unmount
428
+
429
+ ##
430
+ # Finds a servlet for +path+
431
+
432
+ def search_servlet(path)
433
+
434
+ script_name, path_info = @mount_tab.scan(path)
435
+ servlet, options = @mount_tab[script_name]
436
+ if servlet
437
+ [ servlet, options, script_name, path_info ]
438
+ end
439
+ end
440
+
441
+ def access_log(config, req, res)
442
+ param = ::WEBrickNIO::AccessLog::setup_params(config, req, res)
443
+ @config[:AccessLog].each{|logger, fmt|
444
+ logger << ::WEBrickNIO::AccessLog::format(fmt+"\n", param)
445
+ }
446
+ end
447
+
448
+ class MountTable
449
+ def initialize
450
+ @tab = Hash.new
451
+ compile
452
+ end
453
+
454
+ def [](dir)
455
+ dir = normalize(dir)
456
+ @tab[dir]
457
+ end
458
+
459
+ def []=(dir, val)
460
+ dir = normalize(dir)
461
+ @tab[dir] = val
462
+ compile
463
+ val
464
+ end
465
+
466
+ def delete(dir)
467
+ dir = normalize(dir)
468
+ res = @tab.delete(dir)
469
+ compile
470
+ res
471
+ end
472
+
473
+ def scan(path)
474
+ @scanner =~ path
475
+ [ $&, $' ]
476
+ end
477
+
478
+ private
479
+
480
+ def compile
481
+ k = @tab.keys
482
+ k.sort!
483
+ k.reverse!
484
+ k.collect!{|path| Regexp.escape(path) }
485
+ @scanner = Regexp.new("^(" + k.join("|") +")(?=/|$)")
486
+ end
487
+
488
+ def normalize(dir)
489
+ ret = dir ? dir.dup : ""
490
+ ret.sub!(%r|/+$|, "")
491
+ ret
492
+ end
493
+ end
494
+
495
+ private
496
+ MAX_URI_LENGTH = 2083
497
+
498
+ end
499
+ end