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.
Files changed (47) hide show
  1. data/.gitignore +4 -0
  2. data/.rvmrc +1 -0
  3. data/Gemfile +27 -0
  4. data/LICENSE.txt +14 -0
  5. data/README.markdown +344 -0
  6. data/Rakefile +38 -0
  7. data/bin/gopher2000 +51 -0
  8. data/examples/default_route.rb +22 -0
  9. data/examples/nyan.rb +62 -0
  10. data/examples/simple.rb +147 -0
  11. data/examples/twitter.rb +61 -0
  12. data/examples/weather.rb +69 -0
  13. data/gopher2000.gemspec +35 -0
  14. data/lib/gopher2000/base.rb +552 -0
  15. data/lib/gopher2000/dispatcher.rb +81 -0
  16. data/lib/gopher2000/dsl.rb +128 -0
  17. data/lib/gopher2000/errors.rb +14 -0
  18. data/lib/gopher2000/handlers/base_handler.rb +18 -0
  19. data/lib/gopher2000/handlers/directory_handler.rb +125 -0
  20. data/lib/gopher2000/rendering/abstract_renderer.rb +10 -0
  21. data/lib/gopher2000/rendering/base.rb +174 -0
  22. data/lib/gopher2000/rendering/menu.rb +129 -0
  23. data/lib/gopher2000/rendering/text.rb +10 -0
  24. data/lib/gopher2000/request.rb +21 -0
  25. data/lib/gopher2000/response.rb +25 -0
  26. data/lib/gopher2000/server.rb +85 -0
  27. data/lib/gopher2000/version.rb +4 -0
  28. data/lib/gopher2000.rb +33 -0
  29. data/scripts/god.rb +8 -0
  30. data/spec/application_spec.rb +54 -0
  31. data/spec/dispatching_spec.rb +144 -0
  32. data/spec/dsl_spec.rb +116 -0
  33. data/spec/gopher_spec.rb +1 -0
  34. data/spec/handlers/directory_handler_spec.rb +116 -0
  35. data/spec/helpers_spec.rb +16 -0
  36. data/spec/rendering/base_spec.rb +59 -0
  37. data/spec/rendering/menu_spec.rb +109 -0
  38. data/spec/rendering_spec.rb +84 -0
  39. data/spec/request_spec.rb +30 -0
  40. data/spec/response_spec.rb +33 -0
  41. data/spec/routing_spec.rb +92 -0
  42. data/spec/sandbox/old/socks.txt +0 -0
  43. data/spec/sandbox/socks.txt +0 -0
  44. data/spec/server_spec.rb +127 -0
  45. data/spec/spec_helper.rb +52 -0
  46. data/specs.watchr +60 -0
  47. 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