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.
- data/LICENSE +26 -0
- data/README.md +58 -0
- data/lib/generator/USAGE +8 -0
- data/lib/generator/webricknio.rb +57 -0
- data/lib/generator/webricknio_generator.rb +15 -0
- data/lib/rack/handler/webricknio.rb +88 -0
- data/lib/webricknio.rb +237 -0
- data/lib/webricknio/accesslog.rb +157 -0
- data/lib/webricknio/block.rb +271 -0
- data/lib/webricknio/block.yaml +7 -0
- data/lib/webricknio/config.rb +125 -0
- data/lib/webricknio/httprequest.rb +559 -0
- data/lib/webricknio/httpresponse.rb +469 -0
- data/lib/webricknio/httpserver.rb +499 -0
- data/lib/webricknio/log.rb +30 -0
- data/lib/webricknio/version.rb +14 -0
- data/webricknio.gemspec +15 -0
- metadata +79 -0
@@ -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
|