gopher2000 0.1.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/.gitignore +4 -0
- data/.rvmrc +1 -0
- data/Gemfile +27 -0
- data/LICENSE.txt +14 -0
- data/README.markdown +344 -0
- data/Rakefile +38 -0
- data/bin/gopher2000 +51 -0
- data/examples/default_route.rb +22 -0
- data/examples/nyan.rb +62 -0
- data/examples/simple.rb +147 -0
- data/examples/twitter.rb +61 -0
- data/examples/weather.rb +69 -0
- data/gopher2000.gemspec +35 -0
- data/lib/gopher2000/base.rb +552 -0
- data/lib/gopher2000/dispatcher.rb +81 -0
- data/lib/gopher2000/dsl.rb +128 -0
- data/lib/gopher2000/errors.rb +14 -0
- data/lib/gopher2000/handlers/base_handler.rb +18 -0
- data/lib/gopher2000/handlers/directory_handler.rb +125 -0
- data/lib/gopher2000/rendering/abstract_renderer.rb +10 -0
- data/lib/gopher2000/rendering/base.rb +174 -0
- data/lib/gopher2000/rendering/menu.rb +129 -0
- data/lib/gopher2000/rendering/text.rb +10 -0
- data/lib/gopher2000/request.rb +21 -0
- data/lib/gopher2000/response.rb +25 -0
- data/lib/gopher2000/server.rb +85 -0
- data/lib/gopher2000/version.rb +4 -0
- data/lib/gopher2000.rb +33 -0
- data/scripts/god.rb +8 -0
- data/spec/application_spec.rb +54 -0
- data/spec/dispatching_spec.rb +144 -0
- data/spec/dsl_spec.rb +116 -0
- data/spec/gopher_spec.rb +1 -0
- data/spec/handlers/directory_handler_spec.rb +116 -0
- data/spec/helpers_spec.rb +16 -0
- data/spec/rendering/base_spec.rb +59 -0
- data/spec/rendering/menu_spec.rb +109 -0
- data/spec/rendering_spec.rb +84 -0
- data/spec/request_spec.rb +30 -0
- data/spec/response_spec.rb +33 -0
- data/spec/routing_spec.rb +92 -0
- data/spec/sandbox/old/socks.txt +0 -0
- data/spec/sandbox/socks.txt +0 -0
- data/spec/server_spec.rb +127 -0
- data/spec/spec_helper.rb +52 -0
- data/specs.watchr +60 -0
- metadata +211 -0
@@ -0,0 +1,552 @@
|
|
1
|
+
module Gopher
|
2
|
+
|
3
|
+
#
|
4
|
+
# main application class for a gopher server. holds all the
|
5
|
+
# methods/data required to interact with clients.
|
6
|
+
#
|
7
|
+
class Application
|
8
|
+
|
9
|
+
# The output pattern we will use to generate access logs
|
10
|
+
ACCESS_LOG_PATTERN = "%d\t%m\n"
|
11
|
+
|
12
|
+
@@access_log = nil
|
13
|
+
@@debug_log = nil
|
14
|
+
|
15
|
+
@routes = []
|
16
|
+
@menus = {}
|
17
|
+
@text_templates = {}
|
18
|
+
@scripts ||= []
|
19
|
+
|
20
|
+
attr_accessor :menus, :text_templates, :routes, :config, :scripts, :last_reload, :params, :request
|
21
|
+
|
22
|
+
|
23
|
+
#
|
24
|
+
# reset the app. clear out any routes, templates, config values,
|
25
|
+
# etc. this is used during the load process
|
26
|
+
#
|
27
|
+
def reset!
|
28
|
+
self.routes = []
|
29
|
+
self.menus = {}
|
30
|
+
self.text_templates = {}
|
31
|
+
self.scripts ||= []
|
32
|
+
self.config ||= {
|
33
|
+
:debug => false,
|
34
|
+
:host => "0.0.0.0",
|
35
|
+
:port => 70
|
36
|
+
}
|
37
|
+
|
38
|
+
register_defaults
|
39
|
+
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# return the host we will use when outputting gopher menus
|
45
|
+
#
|
46
|
+
def host
|
47
|
+
config[:host] ||= '0.0.0.0'
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# return the port we will use when outputting gopher menus
|
52
|
+
#
|
53
|
+
def port
|
54
|
+
config[:port] ||= 70
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
# are we in debugging mode?
|
59
|
+
#
|
60
|
+
def debug_mode?
|
61
|
+
config[:debug] == true
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
#
|
66
|
+
# check if our script has been updated since the last reload
|
67
|
+
#
|
68
|
+
def should_reload?
|
69
|
+
! last_reload.nil? && self.scripts.any? do |f|
|
70
|
+
File.mtime(f) > last_reload
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
#
|
75
|
+
# reload scripts if needed
|
76
|
+
#
|
77
|
+
def reload_stale
|
78
|
+
reload_check = should_reload?
|
79
|
+
self.last_reload = Time.now
|
80
|
+
|
81
|
+
return if ! reload_check
|
82
|
+
reset!
|
83
|
+
|
84
|
+
self.scripts.each do |f|
|
85
|
+
debug_log "reload #{f}"
|
86
|
+
load f
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
#
|
92
|
+
# mount a directory for browsing via gopher
|
93
|
+
#
|
94
|
+
# @param [Hash] A hash specifying the path your route will answer to, and the filesystem path to use '/route' => '/home/path/etc'
|
95
|
+
#
|
96
|
+
# @param [Hash] a hash of options for the mount. Primarily this is a filter, which will restrict the list files outputted. example: :filter => '*.jpg'
|
97
|
+
#
|
98
|
+
# @example mount the directory '/home/user/foo' at the gopher path '/files', and only show JPG files:
|
99
|
+
# mount '/files' => '/home/user/foo', :filter => '*.jpg'
|
100
|
+
#
|
101
|
+
def mount(path, opts = {}, klass = Gopher::Handlers::DirectoryHandler)
|
102
|
+
debug_log "MOUNT #{path} #{opts.inspect}"
|
103
|
+
opts[:mount_point] = path
|
104
|
+
|
105
|
+
handler = klass.new(opts)
|
106
|
+
handler.application = self
|
107
|
+
|
108
|
+
#
|
109
|
+
# add a route for the mounted class
|
110
|
+
#
|
111
|
+
route(globify(path)) do
|
112
|
+
# when we call, pass the params and request object for this
|
113
|
+
# particular request
|
114
|
+
handler.call(params, request)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
#
|
120
|
+
# define a route.
|
121
|
+
# @param [String] the path your route will answer to. This is
|
122
|
+
# basically a URI path
|
123
|
+
# @yield a block that handles your route
|
124
|
+
#
|
125
|
+
# @example respond with a simple string
|
126
|
+
# route '/path' do
|
127
|
+
# "hi, welcome to /path"
|
128
|
+
# end
|
129
|
+
#
|
130
|
+
# @example respond by rendering a template
|
131
|
+
# route '/render' do
|
132
|
+
# render :template
|
133
|
+
# end
|
134
|
+
#
|
135
|
+
def route(path, &block)
|
136
|
+
selector = sanitize_selector(path)
|
137
|
+
sig = compile!(selector, &block)
|
138
|
+
|
139
|
+
debug_log("Add route for #{selector}")
|
140
|
+
|
141
|
+
self.routes ||= []
|
142
|
+
self.routes << sig
|
143
|
+
end
|
144
|
+
|
145
|
+
|
146
|
+
#
|
147
|
+
# specify a default route to handle requests if no other route exists
|
148
|
+
#
|
149
|
+
# @example render a template
|
150
|
+
# default_route do
|
151
|
+
# render :template
|
152
|
+
# end
|
153
|
+
#
|
154
|
+
def default_route(&block)
|
155
|
+
@default_route = Application.generate_method("DEFAULT_ROUTE", &block)
|
156
|
+
end
|
157
|
+
|
158
|
+
#
|
159
|
+
# lookup an incoming path
|
160
|
+
#
|
161
|
+
# @param [String] the selector path of the incoming request
|
162
|
+
#
|
163
|
+
def lookup(selector)
|
164
|
+
unless routes.nil?
|
165
|
+
routes.each do |pattern, keys, block|
|
166
|
+
|
167
|
+
if match = pattern.match(selector)
|
168
|
+
match = match.to_a
|
169
|
+
url = match.shift
|
170
|
+
|
171
|
+
params = to_params_hash(keys, match)
|
172
|
+
|
173
|
+
#
|
174
|
+
# @todo think about this
|
175
|
+
#
|
176
|
+
@params = params
|
177
|
+
|
178
|
+
return params, block
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
unless @default_route.nil?
|
184
|
+
return {}, @default_route
|
185
|
+
end
|
186
|
+
|
187
|
+
raise Gopher::NotFoundError
|
188
|
+
end
|
189
|
+
|
190
|
+
|
191
|
+
#
|
192
|
+
# find and run the first route which matches the incoming request
|
193
|
+
# @param [Request] Gopher::Request object
|
194
|
+
#
|
195
|
+
def dispatch(req)
|
196
|
+
debug_log(req)
|
197
|
+
|
198
|
+
response = Response.new
|
199
|
+
@request = req
|
200
|
+
|
201
|
+
if ! @request.valid?
|
202
|
+
response.body = handle_invalid_request
|
203
|
+
response.code = :error
|
204
|
+
else
|
205
|
+
begin
|
206
|
+
debug_log("do lookup for #{@request.selector}")
|
207
|
+
@params, block = lookup(@request.selector)
|
208
|
+
|
209
|
+
#
|
210
|
+
# call the block that handles this lookup
|
211
|
+
#
|
212
|
+
response.body = block.bind(self).call
|
213
|
+
response.code = :success
|
214
|
+
rescue Gopher::NotFoundError => e
|
215
|
+
debug_log("#{@request.selector} -- not found")
|
216
|
+
response.body = handle_not_found
|
217
|
+
response.code = :missing
|
218
|
+
rescue Exception => e
|
219
|
+
debug_log("#{@request.selector} -- error")
|
220
|
+
debug_log(e.inspect)
|
221
|
+
debug_log(e.backtrace)
|
222
|
+
|
223
|
+
response.body = handle_error(e)
|
224
|
+
response.code = :error
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
access_log(req, response)
|
229
|
+
response
|
230
|
+
end
|
231
|
+
|
232
|
+
#
|
233
|
+
# define a template which will be used to render a gopher-style
|
234
|
+
# menu.
|
235
|
+
#
|
236
|
+
# @param [String/Symbol] -- the name of the template. This is what
|
237
|
+
# identifies the template when making a call to render
|
238
|
+
# @yield a block which will output the menu. This block is
|
239
|
+
# executed within an instance of Gopher::Rendering::Menu and will
|
240
|
+
# have access to all of its methods.
|
241
|
+
#
|
242
|
+
# @example a simple menu:
|
243
|
+
# menu :index do
|
244
|
+
# # output a text entry in the menu
|
245
|
+
# text 'simple gopher example'
|
246
|
+
#
|
247
|
+
# # use br(x) to add x space between lines
|
248
|
+
# br(2)
|
249
|
+
#
|
250
|
+
# # link somewhere
|
251
|
+
# link 'current time', '/time'
|
252
|
+
# br
|
253
|
+
#
|
254
|
+
# # another link
|
255
|
+
# link 'about', '/about'
|
256
|
+
# br
|
257
|
+
#
|
258
|
+
# # ask for some input
|
259
|
+
# input 'Hey, what is your name?', '/hello'
|
260
|
+
# br
|
261
|
+
#
|
262
|
+
# # mount some files
|
263
|
+
# menu 'filez', '/files'
|
264
|
+
# end
|
265
|
+
#
|
266
|
+
def menu(name, &block)
|
267
|
+
menus[name.to_sym] = block
|
268
|
+
end
|
269
|
+
|
270
|
+
#
|
271
|
+
# Define a template which will be used for outputting text. This
|
272
|
+
# is not strictly required for outputting text, but it gives you
|
273
|
+
# access to the methods defined in Gopher::Rendering::Text for
|
274
|
+
# wrapping strings, adding simple headers, etc.
|
275
|
+
#
|
276
|
+
# @param [String/Symbol] -- the name of the template. This is what identifies the template when making a call to render
|
277
|
+
#
|
278
|
+
# @yield a block which will output the menu. This block is executed within an instance of Gopher::Rendering::Text and will have access to all of its methods.
|
279
|
+
# @example simple example
|
280
|
+
# text :hello do
|
281
|
+
# big_header "Hello There!"
|
282
|
+
# block "Really long text... ... the end"
|
283
|
+
# br
|
284
|
+
# end
|
285
|
+
def text(name, &block)
|
286
|
+
text_templates[name.to_sym] = block
|
287
|
+
end
|
288
|
+
|
289
|
+
#
|
290
|
+
# specify a template to be used for missing requests
|
291
|
+
#
|
292
|
+
def not_found(&block)
|
293
|
+
menu :not_found, &block
|
294
|
+
end
|
295
|
+
|
296
|
+
#
|
297
|
+
# find a template
|
298
|
+
# @param [String/Symbol] name of the template
|
299
|
+
# @return template block and the class context it should use
|
300
|
+
#
|
301
|
+
def find_template(t)
|
302
|
+
x = menus[t]
|
303
|
+
if x
|
304
|
+
return x, Gopher::Rendering::Menu
|
305
|
+
end
|
306
|
+
x = text_templates[t]
|
307
|
+
if x
|
308
|
+
return x, Gopher::Rendering::Text
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
#
|
313
|
+
# Find the desired template and call it within the proper context
|
314
|
+
# @param [String/Symbol] name of the template to render
|
315
|
+
# @param [Array] optional arguments to be passed to template
|
316
|
+
# @return result of rendering
|
317
|
+
#
|
318
|
+
def render(template, *arguments)
|
319
|
+
#
|
320
|
+
# find the right renderer we need
|
321
|
+
#
|
322
|
+
block, handler = find_template(template)
|
323
|
+
|
324
|
+
raise TemplateNotFound if block.nil?
|
325
|
+
|
326
|
+
ctx = handler.new(self)
|
327
|
+
ctx.params = @params
|
328
|
+
ctx.request = @request
|
329
|
+
|
330
|
+
ctx.instance_exec(*arguments, &block)
|
331
|
+
end
|
332
|
+
|
333
|
+
#
|
334
|
+
# get the id of the template that will be used when rendering a
|
335
|
+
# not found error
|
336
|
+
# @return name of not_found template
|
337
|
+
#
|
338
|
+
def not_found_template
|
339
|
+
menus.include?(:not_found) ? :not_found : :'internal/not_found'
|
340
|
+
end
|
341
|
+
|
342
|
+
#
|
343
|
+
# get the id of the template that will be used when rendering an error
|
344
|
+
# @return name of error template
|
345
|
+
#
|
346
|
+
def error_template
|
347
|
+
menus.include?(:error) ? :error : :'internal/error'
|
348
|
+
end
|
349
|
+
|
350
|
+
#
|
351
|
+
# get the id of the template that will be used when rendering an
|
352
|
+
# invalid request
|
353
|
+
# @return name of invalid_request template
|
354
|
+
#
|
355
|
+
def invalid_request_template
|
356
|
+
menus.include?(:invalid_request) ? :invalid_request : :'internal/invalid_request'
|
357
|
+
end
|
358
|
+
|
359
|
+
|
360
|
+
#
|
361
|
+
# Add helpers to the Base renedering class, which allows them to be called
|
362
|
+
# when outputting the results of an action. Here's the code in Sinatra for reference:
|
363
|
+
#
|
364
|
+
# Makes the methods defined in the block and in the Modules given
|
365
|
+
# in `extensions` available to the handlers and templates
|
366
|
+
# def helpers(*extensions, &block)
|
367
|
+
# class_eval(&block) if block_given?
|
368
|
+
# include(*extensions) if extensions.any?
|
369
|
+
# end
|
370
|
+
#
|
371
|
+
# target - What class should receive the helpers -- defaults to Gopher::Rendering::Base, which will make it available when rendering
|
372
|
+
# block -- a block which declares the helpers you want. for example:
|
373
|
+
#
|
374
|
+
# helpers do
|
375
|
+
# def foo; "FOO"; end
|
376
|
+
# end
|
377
|
+
def helpers(target = Gopher::Application, &block)
|
378
|
+
target.class_eval(&block)
|
379
|
+
end
|
380
|
+
|
381
|
+
|
382
|
+
#
|
383
|
+
# should we use non-blocking operations? for now, defaults to false if in debug mode,
|
384
|
+
# true if we're not in debug mode (presumably, in some sort of production state. HAH!
|
385
|
+
# Gopher servers in production)
|
386
|
+
#
|
387
|
+
def non_blocking?
|
388
|
+
config[:non_blocking] ||= ! debug_mode?
|
389
|
+
end
|
390
|
+
|
391
|
+
|
392
|
+
#
|
393
|
+
# add a glob to the end of this string, if there's not one already
|
394
|
+
#
|
395
|
+
def globify(p)
|
396
|
+
p =~ /\*/ ? p : "#{p}/?*".gsub("//", "/")
|
397
|
+
end
|
398
|
+
|
399
|
+
#
|
400
|
+
# compile a route
|
401
|
+
#
|
402
|
+
def compile!(path, &block)
|
403
|
+
method_name = path
|
404
|
+
route_method = Application.generate_method(method_name, &block)
|
405
|
+
pattern, keys = compile path
|
406
|
+
|
407
|
+
[ pattern, keys, route_method ]
|
408
|
+
end
|
409
|
+
|
410
|
+
#
|
411
|
+
# turn a path string with optional keys (/foo/:bar/:boo) into a
|
412
|
+
# regexp which will be used when searching for a route
|
413
|
+
#
|
414
|
+
# @param [String] the path to compile
|
415
|
+
#
|
416
|
+
def compile(path)
|
417
|
+
keys = []
|
418
|
+
pattern = path.to_str.gsub(/[^\?\%\\\/\:\*\w]/) { |c| encoded(c) }
|
419
|
+
pattern.gsub!(/((:\w+)|\*)/) do |match|
|
420
|
+
if match == "*"
|
421
|
+
keys << 'splat'
|
422
|
+
"(.*?)"
|
423
|
+
else
|
424
|
+
keys << $2[1..-1]
|
425
|
+
"([^/?#]+)"
|
426
|
+
end
|
427
|
+
end
|
428
|
+
[/^#{pattern}$/, keys]
|
429
|
+
end
|
430
|
+
|
431
|
+
#
|
432
|
+
# Sanitizes a gopher selector
|
433
|
+
#
|
434
|
+
def sanitize_selector(raw)
|
435
|
+
raw.to_s.dup.
|
436
|
+
strip. # Strip whitespace
|
437
|
+
sub(/\/$/, ''). # Strip last rslash
|
438
|
+
sub(/^\/*/, '/'). # Strip extra lslashes
|
439
|
+
gsub(/\.+/, '.') # Don't want consecutive dots!
|
440
|
+
end
|
441
|
+
|
442
|
+
class << self
|
443
|
+
#
|
444
|
+
# generate a method which we will use to run routes. this is
|
445
|
+
# based on #generate_method as used by sinatra.
|
446
|
+
# @see https://github.com/sinatra/sinatra/blob/master/lib/sinatra/base.rb
|
447
|
+
# @param [String] name to use for the method
|
448
|
+
# @yield block to use for the method
|
449
|
+
def generate_method(method_name, &block)
|
450
|
+
define_method(method_name, &block)
|
451
|
+
method = instance_method method_name
|
452
|
+
remove_method method_name
|
453
|
+
method
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
#
|
458
|
+
# output a debugging message
|
459
|
+
#
|
460
|
+
def debug_log(x)
|
461
|
+
@@debug_logger ||= ::Logging.logger(STDERR)
|
462
|
+
@@debug_logger.debug x
|
463
|
+
end
|
464
|
+
|
465
|
+
|
466
|
+
protected
|
467
|
+
|
468
|
+
#
|
469
|
+
# set up some default templates to handle errors, missing templates, etc.
|
470
|
+
#
|
471
|
+
def register_defaults
|
472
|
+
menu :'internal/not_found' do
|
473
|
+
text "Sorry, #{@request.selector} was not found"
|
474
|
+
end
|
475
|
+
|
476
|
+
menu :'internal/error' do |details|
|
477
|
+
text "Sorry, there was an error #{details}"
|
478
|
+
end
|
479
|
+
|
480
|
+
menu :'internal/invalid_request' do
|
481
|
+
text "invalid request"
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
def handle_not_found
|
486
|
+
render not_found_template
|
487
|
+
end
|
488
|
+
|
489
|
+
def handle_error(e)
|
490
|
+
render error_template, e
|
491
|
+
end
|
492
|
+
|
493
|
+
def handle_invalid_request
|
494
|
+
render invalid_request_template
|
495
|
+
end
|
496
|
+
|
497
|
+
|
498
|
+
|
499
|
+
#
|
500
|
+
# where should we store access logs? if nil, don't store them at all
|
501
|
+
# @return logfile path
|
502
|
+
#
|
503
|
+
def access_log_dest
|
504
|
+
config.has_key?(:access_log) ? config[:access_log] : nil
|
505
|
+
end
|
506
|
+
|
507
|
+
#
|
508
|
+
# initialize a Logger for tracking hits to the server
|
509
|
+
#
|
510
|
+
def init_access_log
|
511
|
+
return if access_log_dest.nil?
|
512
|
+
|
513
|
+
log = ::Logging.logger['access_log']
|
514
|
+
pattern = ::Logging.layouts.pattern(:pattern => ACCESS_LOG_PATTERN)
|
515
|
+
|
516
|
+
log.add_appenders(
|
517
|
+
::Logging.appenders.rolling_file(access_log_dest,
|
518
|
+
:level => :debug,
|
519
|
+
:age => 'daily',
|
520
|
+
:layout => pattern)
|
521
|
+
)
|
522
|
+
|
523
|
+
log
|
524
|
+
end
|
525
|
+
|
526
|
+
#
|
527
|
+
# write out an entry to our access log
|
528
|
+
#
|
529
|
+
def access_log(request, response)
|
530
|
+
return if access_log_dest.nil?
|
531
|
+
|
532
|
+
@@access_logger ||= init_access_log
|
533
|
+
code = response.respond_to?(:code) ? response.code.to_s : "success"
|
534
|
+
size = response.respond_to?(:size) ? response.size : response.length
|
535
|
+
output = [request.ip_address, request.selector, request.input, code.to_s, size].join("\t")
|
536
|
+
|
537
|
+
@@access_logger.debug output
|
538
|
+
end
|
539
|
+
|
540
|
+
|
541
|
+
#
|
542
|
+
# zip up two arrays of keys and values from an incoming request
|
543
|
+
#
|
544
|
+
def to_params_hash(keys,values)
|
545
|
+
hash = {}
|
546
|
+
keys.size.times { |i| hash[ keys[i].to_sym ] = values[i] }
|
547
|
+
hash
|
548
|
+
end
|
549
|
+
|
550
|
+
|
551
|
+
end
|
552
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Gopher
|
2
|
+
|
3
|
+
#
|
4
|
+
# Handle communication between Server and the actual gopher Application
|
5
|
+
#
|
6
|
+
class Dispatcher < EventMachine::Connection
|
7
|
+
|
8
|
+
# the Application we are running
|
9
|
+
attr_accessor :app
|
10
|
+
|
11
|
+
#
|
12
|
+
# get the IP address of the client
|
13
|
+
# @return ip address
|
14
|
+
#
|
15
|
+
def remote_ip
|
16
|
+
Socket.unpack_sockaddr_in(get_peername).last
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
#
|
21
|
+
# called by EventMachine when there's an incoming request
|
22
|
+
#
|
23
|
+
# @param [String] incoming selector
|
24
|
+
# @return Response object
|
25
|
+
#
|
26
|
+
def receive_data(selector)
|
27
|
+
call! Request.new(selector, remote_ip)
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# generate a request object from an incoming selector, and dispatch it to the app
|
32
|
+
# @param [String] incoming selector
|
33
|
+
# @return Response object
|
34
|
+
#
|
35
|
+
def call!(request)
|
36
|
+
operation = proc {
|
37
|
+
app.dispatch(request)
|
38
|
+
}
|
39
|
+
callback = proc {|result|
|
40
|
+
send_response result
|
41
|
+
close_connection_after_writing
|
42
|
+
}
|
43
|
+
|
44
|
+
#
|
45
|
+
# if we don't want to block on slow calls, use EM#defer
|
46
|
+
# @see http://eventmachine.rubyforge.org/EventMachine.html#M000486
|
47
|
+
#
|
48
|
+
if app.non_blocking?
|
49
|
+
EventMachine.defer( operation, callback )
|
50
|
+
else
|
51
|
+
callback.call(operation.call)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
#
|
56
|
+
# send the response back to the client
|
57
|
+
# @param [Response] response object
|
58
|
+
#
|
59
|
+
def send_response(response)
|
60
|
+
case response
|
61
|
+
when Gopher::Response then send_response(response.body)
|
62
|
+
when String then send_data(response + end_of_transmission)
|
63
|
+
when StringIO then send_data(response.read + end_of_transmission)
|
64
|
+
when File
|
65
|
+
while chunk = response.read(8192) do
|
66
|
+
send_data(chunk)
|
67
|
+
end
|
68
|
+
response.close
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
#
|
73
|
+
# Add the period on a line by itself that closes the connection
|
74
|
+
#
|
75
|
+
# @return valid string to mark end of transmission as specified in RFC1436
|
76
|
+
def end_of_transmission
|
77
|
+
[Gopher::Rendering::LINE_ENDING, ".", Gopher::Rendering::LINE_ENDING].join
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|