webricknio 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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