gopher2000 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,128 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'gopher2000')
2
+
3
+ module Gopher
4
+ #
5
+ # DSL that can be used to specify gopher apps with a very simple format.
6
+ # @see the examples/ directory for working scripts
7
+ #
8
+ module DSL
9
+ @application = nil
10
+
11
+ #
12
+ # initialize an instance of an Application if we haven't already, otherwise, return
13
+ # the current app
14
+ # @return [Gopher::Application] current app
15
+ #
16
+ def application
17
+ return @application unless @application.nil?
18
+
19
+ @application = Gopher::Application.new
20
+ @application.reset!
21
+ end
22
+
23
+ # set a config value
24
+ # @param [Symbol] key key to add to config
25
+ # @param [String] value value to set
26
+ def set(key, value = nil)
27
+ application.config[key] = value
28
+ end
29
+
30
+ # specify a route
31
+ # @param [String] path path of the route
32
+ # @yield block that will respond to the request
33
+ def route(path, &block)
34
+ application.route(path, &block)
35
+ end
36
+
37
+ # specify a default route
38
+ def default_route(&block)
39
+ application.default_route(&block)
40
+ end
41
+
42
+ # mount a folder for browsing
43
+ def mount(path, opts = {})
44
+ route, folder = path.first
45
+
46
+ #
47
+ # if path has more than the one option (:route => :folder),
48
+ # then incorporate the rest of the hash into our opts
49
+ #
50
+ if path.size > 1
51
+ other_opts = path.dup
52
+ other_opts.delete(route)
53
+ opts = opts.merge(other_opts)
54
+ end
55
+
56
+ application.mount(route, opts.merge({:path => folder}))
57
+ end
58
+
59
+ # specify a menu template
60
+ # @param [Symbol] name of the template
61
+ # @yield block which renders the template
62
+ def menu(name, &block)
63
+ application.menu(name, &block)
64
+ end
65
+
66
+ # specify a text template
67
+ # @param [Symbol] name of the template
68
+ # @yield block which renders the template
69
+ def text(name, &block)
70
+ application.text(name, &block)
71
+ end
72
+
73
+ # def template(name, &block)
74
+ # application.template(name, &block)
75
+ # end
76
+
77
+ # specify some helpers for your app
78
+ # @yield block which defines the helper methods
79
+ def helpers(&block)
80
+ application.helpers(&block)
81
+ end
82
+
83
+ # watch the specified script for changes
84
+ # @param [String] script to watch
85
+ def watch(f)
86
+ application.scripts << f
87
+ end
88
+
89
+ #
90
+ # run a script with the specified options applied to the config. This is
91
+ # called by bin/gopher2000
92
+ # @param [String] script path to script to run
93
+ # @param [Hash] opts options to pass to script. these will override any
94
+ # config options specified in the script, so you can use this to
95
+ # run on a different host/port, etc.
96
+ #
97
+ def run(script, opts = {})
98
+
99
+ load script
100
+
101
+ #
102
+ # apply options after loading the script so that anything specified on the command-line
103
+ # will take precedence over defaults specified in the script
104
+ #
105
+ opts.each { |k, v|
106
+ set k, v
107
+ }
108
+
109
+ if application.config[:debug] == true
110
+ puts "watching #{script} for changes"
111
+ watch script
112
+ end
113
+
114
+ end
115
+ end
116
+ end
117
+
118
+ include Gopher::DSL
119
+
120
+ #
121
+ # don't call at_exit if we're running specs
122
+ #
123
+ unless ENV['gopher_test']
124
+ at_exit do
125
+ s = Gopher::Server.new(@application)
126
+ s.run!
127
+ end
128
+ end
@@ -0,0 +1,14 @@
1
+ module Gopher
2
+
3
+ # base error class
4
+ class GopherError < StandardError; end
5
+
6
+ # When a selector isn't found in the route map
7
+ class NotFoundError < GopherError; end
8
+
9
+ # Invalid gopher requests
10
+ class InvalidRequest < GopherError; end
11
+
12
+ # Template not found in local or global space
13
+ class TemplateNotFound < GopherError; end
14
+ end
@@ -0,0 +1,18 @@
1
+ module Gopher
2
+
3
+ #
4
+ # namespace for custom handlers
5
+ #
6
+ module Handlers
7
+ #
8
+ # Base class for custom request handlers. Any custom handler
9
+ # code should inherit from this class.
10
+ #
11
+ class BaseHandler
12
+ attr_accessor :application
13
+
14
+ # include rendering here so that Menu/Text renderers are available to any handlers
15
+ include Rendering
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,125 @@
1
+ require 'pathname'
2
+
3
+ module Gopher
4
+ module Handlers
5
+ #
6
+ # handle browsing a directory structure/returning files to the client
7
+ #
8
+ class DirectoryHandler < BaseHandler
9
+
10
+ attr_accessor :path, :filter, :mount_point
11
+
12
+ #
13
+ # @option opts [String] :filter a subset of files to show to user
14
+ # @option opts [String] :path the base path of the filesystem to work from
15
+ # @option opts [String] :mount_point the route for this handler -- this will be used to generate paths in the response
16
+ #
17
+ def initialize(opts = {})
18
+ opts = {
19
+ :filter => "*.*",
20
+ :path => Dir.getwd
21
+ }.merge(opts)
22
+
23
+ @path = opts[:path]
24
+ @filter = opts[:filter]
25
+ @mount_point = opts[:mount_point]
26
+ end
27
+
28
+ #
29
+ # strip slashes, extra dots, etc, from an incoming selector and turn it into a 'normalized' path
30
+ # @param [String] path
31
+ # @return clean path string
32
+ #
33
+ def sanitize(p)
34
+ Pathname.new(p).cleanpath.to_s
35
+ end
36
+
37
+ #
38
+ # make sure that the requested file is actually contained within our mount point. this
39
+ # prevents requests like the below from working:
40
+ #
41
+ # echo "/files/../../../../../tmp/foo" | nc localhost 7070
42
+ #
43
+ def contained?(p)
44
+ (p =~ /^#{@path}/) != nil
45
+ end
46
+
47
+ #
48
+ # take the incoming parameters, and turn them into a path
49
+ # @option opts [String] :splat splat value from Request params
50
+ #
51
+ def request_path(params)
52
+ File.absolute_path(sanitize(params[:splat]), @path)
53
+ end
54
+
55
+ #
56
+ # take the path to a file and turn it into a selector which will match up when
57
+ # a gopher client makes requests
58
+ # @param [String] path to a file on the filesytem
59
+ # @return selector which will match the file on subsequent requests
60
+ #
61
+ def to_selector(path)
62
+ path.gsub(/^#{@path}/, @mount_point)
63
+ end
64
+
65
+ #
66
+ # handle a request
67
+ #
68
+ # @param [Hash] the params as parsed during the dispatching process - the main thing here should be :splat, which will basically be the path requested.
69
+ # @param [Request] - the Request object for this session -- not currently used?
70
+ #
71
+ def call(params = {}, request = nil)
72
+ # debug_log "DirectoryHandler: call #{params.inspect}, #{request.inspect}"
73
+
74
+ lookup = request_path(params)
75
+
76
+ raise Gopher::InvalidRequest if ! contained?(lookup)
77
+
78
+ if File.directory?(lookup)
79
+ directory(lookup)
80
+ elsif File.file?(lookup)
81
+ file(lookup)
82
+ else
83
+ raise Gopher::NotFoundError
84
+ end
85
+ end
86
+
87
+ #
88
+ # generate a directory listing
89
+ # @param [String] path to directory
90
+ # @return rendered directory output for a response
91
+ #
92
+ def directory(dir)
93
+ m = Menu.new(@application)
94
+
95
+ m.text "Browsing: #{dir}"
96
+
97
+ #
98
+ # iterate through the contents of this directory.
99
+ # NOTE: we don't filter this, so we will ALWAYS list subdirectories of a mounted folder
100
+ #
101
+ Dir.glob("#{dir}/*.*").each do |x|
102
+ # if this is a directory, then generate a directory link for it
103
+ if File.directory?(x)
104
+ m.directory File.basename(x), to_selector(x), @application.host, @application.port
105
+
106
+ elsif File.file?(x) && File.fnmatch(filter, x)
107
+ # fnmatch makes sure that the file matches the glob filter specified in the mount directive
108
+
109
+ # otherwise, it's a normal file link
110
+ m.link File.basename(x), to_selector(x), @application.host, @application.port
111
+ end
112
+ end
113
+ m.to_s
114
+ end
115
+
116
+ #
117
+ # return a file handle -- Connection will take this and send it back to the client
118
+ #
119
+ def file(f)
120
+ File.new(f)
121
+ end
122
+
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,10 @@
1
+ module Gopher
2
+ module Rendering
3
+ #
4
+ # Abstract class for rendering. This is basically overkill right now, so..
5
+ # @todo consider refactoring out
6
+ #
7
+ class AbstractRenderer
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,174 @@
1
+ module Gopher
2
+
3
+ #
4
+ # namespace for classes that render output for the app
5
+ #
6
+ module Rendering
7
+
8
+ # "A CR LF denotes the end of the item." RFC 1436
9
+ # @see http://www.faqs.org/rfcs/rfc1436.html
10
+ LINE_ENDING = "\r\n"
11
+
12
+ #
13
+ # base class for rendering output. this class provides methods
14
+ # that can be used when rendering both text and gopher menus
15
+ #
16
+ class Base < AbstractRenderer
17
+ attr_accessor :result, :spacing, :width, :request, :params, :application
18
+
19
+ def initialize(app=nil)
20
+ @application = app
21
+ @result = ""
22
+ @spacing = 1
23
+
24
+ # default to 70 per RFC1436 3.9
25
+ # "the user display string should be kept under 70 characters in length"
26
+ @width = 70
27
+ end
28
+
29
+ #
30
+ # add a line to the output
31
+ # @param [String] string text to add to the output
32
+ #
33
+ def <<(string)
34
+ @result << string.to_s
35
+ end
36
+
37
+ #
38
+ # Adds +text+ to the result
39
+ # @param[String] text text to add to the result. Adds the line,
40
+ # then adds any required spacing
41
+ #
42
+ def text(text)
43
+ self << text
44
+ add_spacing
45
+ end
46
+
47
+ #
48
+ # specify the desired width of text output -- defaults to 70 chars
49
+ # @param [Integer] n desired width for text output
50
+ #
51
+ def width(n)
52
+ @width = n.to_i
53
+ end
54
+
55
+ #
56
+ # specify spacing between lines.
57
+ #
58
+ # @param [Integer] n desired line spacing
59
+ #
60
+ # @example to make something double-spaced, you could call:
61
+ # spacing(2)
62
+ #
63
+ def spacing(n)
64
+ @spacing = n.to_i
65
+ end
66
+
67
+ #
68
+ # Add some empty lines to the output
69
+ # @param [Integer] n how many lines to add
70
+ #
71
+ def br(n=1)
72
+ self << (LINE_ENDING * n)
73
+ end
74
+
75
+ #
76
+ # wrap +text+ into lines no wider than +width+. Hacked from ActionView
77
+ # @see https://github.com/rails/rails/blob/196407c54f0736c275d2ad4e6f8b0ac55360ad95/actionpack/lib/action_view/helpers/text_helper.rb#L217
78
+ #
79
+ # @param [String] text the text you want to wrap
80
+ # @param [Integer] width the desired width of the block -- defaults to the
81
+ # current output width
82
+ #
83
+ def block(text, width=@width)
84
+
85
+ # this is a hack - recombine lines, then re-split on newlines
86
+ # doing this because word_wrap is returning an array of lines, but
87
+ # those lines have newlines in them where we should wrap
88
+ lines = word_wrap(text, width).join("\n").split("\n")
89
+
90
+ lines.each do |line|
91
+ text line.lstrip.rstrip
92
+ end
93
+
94
+ self.to_s
95
+ end
96
+
97
+ #
98
+ # output a centered string with a nice underline below it,
99
+ # centered on the current output width
100
+ #
101
+ # @param [String] str - the string to output
102
+ # @param [String] under - the desired underline character
103
+ # @param [Boolean] edge - should we output an edge? if so, there will be a
104
+ # character to the left/right edges of the string, so you can
105
+ # draw a box around the text
106
+ #
107
+ def header(str, under = '=', edge = false)
108
+ w = @width
109
+ if edge
110
+ w -= 2
111
+ end
112
+
113
+ tmp = str.center(w)
114
+ if edge
115
+ tmp = "#{under}#{tmp}#{under}"
116
+ end
117
+
118
+ text(tmp)
119
+ underline(@width, under)
120
+ end
121
+
122
+ #
123
+ # output a centered string in a box
124
+ # @param [String] str the string to output
125
+ # @param [Strnig] under the character to use to make the box
126
+ #
127
+ def big_header(str, under = '=')
128
+ br
129
+ underline(@width, under)
130
+ header(str, under, true)
131
+
132
+ # enforcing some extra space around headers for now
133
+ br
134
+ end
135
+
136
+ #
137
+ # output an underline
138
+ #
139
+ # @param [Integer] length the length of the underline -- defaults to current
140
+ # output width.
141
+ # @param [String] char the character to output
142
+ #
143
+ def underline(length=@width, char='=')
144
+ text(char * length)
145
+ end
146
+
147
+
148
+ #
149
+ # return the output as a string
150
+ # @return rendered output
151
+ #
152
+ def to_s
153
+ @result
154
+ end
155
+
156
+ protected
157
+ #
158
+ # borrowed and modified from ActionView -- wrap text at specified width
159
+ # returning an array of lines for now in case we want to do nifty processing with them
160
+ #
161
+ # File actionpack/lib/action_view/helpers/text_helper.rb, line 217
162
+ def word_wrap(text, width=70*args)
163
+ text.split("\n").collect do |line|
164
+ line.length > width ? line.gsub(/(.{1,#{width}})(\s+|$)/, "\\1\n").strip : line
165
+ end
166
+ end
167
+
168
+ private
169
+ def add_spacing
170
+ br(@spacing)
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,129 @@
1
+ module Gopher
2
+ module Rendering
3
+ #
4
+ # The MenuContext is for rendering gopher menus in the "pseudo
5
+ # file-system hierarchy" defined by RFC1436
6
+ #
7
+ # @see http://www.ietf.org/rfc/rfc1436.txt
8
+ #
9
+ class Menu < Base
10
+
11
+ # default host value when rendering a line with no selector
12
+ NO_HOST = '(FALSE)'
13
+
14
+ # default port value when rendering a line with no selector
15
+ NO_PORT = 0
16
+
17
+ # Sanitizes text for use in gopher menus
18
+ # @param [String] text text to cleanup
19
+ # @return string that can be used in a gopher menu
20
+ def sanitize_text(raw)
21
+ raw.
22
+ rstrip. # Remove excess whitespace
23
+ gsub(/\t/, ' ' * 8). # Tabs to spaces
24
+ gsub(/\n/, '') # Get rid of newlines (\r as well?)
25
+ end
26
+
27
+ #
28
+ # output a gopher menu line
29
+ #
30
+ # @param [String] type what sort of entry is this? @see http://www.ietf.org/rfc/rfc1436.txt for a list
31
+ # @param [String] text the text of the line
32
+ # @param [String] selector if this is a link, the path of the route we are linking to
33
+ # @param [String] host for link, defaults to current host
34
+ # @param [String] port for link, defaults to current port
35
+ def line(type, text, selector, host=nil, port=nil)
36
+ text = sanitize_text(text)
37
+
38
+ host = application.host if host.nil?
39
+ port = application.port if port.nil?
40
+
41
+ self << ["#{type}#{text}", selector, host, port].join("\t") + LINE_ENDING
42
+ end
43
+
44
+ #
45
+ # output a line of text, with no selector
46
+ # @param [String] text the text of the line
47
+ # @param [String] type what sort of entry is this? @see http://www.ietf.org/rfc/rfc1436.txt for a list
48
+ #
49
+ def text(text, type = 'i')
50
+ line type, text, 'null', NO_HOST, NO_PORT
51
+ end
52
+
53
+ #
54
+ # add some empty lines to the menu
55
+ # @param [integer] n how many breaks to add
56
+ #
57
+ def br(n=1)
58
+ 1.upto(n) do
59
+ text 'i', ""
60
+ end
61
+ self.to_s
62
+ end
63
+
64
+ #
65
+ # output an error message
66
+ # @param [String] msg text of the message
67
+ #
68
+ def error(msg)
69
+ text(msg, '3')
70
+ end
71
+
72
+ #
73
+ # output a link to a sub-menu/directory
74
+ # @param [String] name of the menu/directory
75
+ # @param [String] selector we are linking to
76
+ # @param [String] host for link, defaults to current host
77
+ # @param [String] port for link, defaults to current port
78
+ #
79
+ def directory(name, selector, host=nil, port=nil)
80
+ line '1', name, selector, host, port
81
+ end
82
+ alias menu directory
83
+
84
+
85
+ #
86
+ # output a menu link
87
+ #
88
+ # @param [String] text the text of the link
89
+ # @param [String] selector the path of the link. the extension of this path will be used to
90
+ # detemine the type of link -- image, archive, etc. If you want
91
+ # to specify a specific link-type, you should use the text
92
+ # method instead
93
+ # @param [String] host for link, defaults to current host
94
+ # @param [String] port for link, defaults to current port
95
+ def link(text, selector, host=nil, port=nil)
96
+ type = determine_type(selector)
97
+ line type, text, selector, host, port
98
+ end
99
+
100
+ #
101
+ # output a search entry
102
+ # @param [String] text the text of the link
103
+ # @param [String] selector the path of the selector
104
+ def search(text, selector, *args)
105
+ line '7', text, selector, *args
106
+ end
107
+ alias input search
108
+
109
+
110
+ #
111
+ # Determines the gopher type for +selector+ based on the
112
+ # extension. This is a pretty simple check based on the entities
113
+ # list in http://www.ietf.org/rfc/rfc1436.txt
114
+ # @param [String] selector, presumably a link to a file name with an extension
115
+ # @return gopher selector type
116
+ #
117
+ def determine_type(selector)
118
+ ext = File.extname(selector).downcase
119
+ case ext
120
+ when '.zip', '.gz', '.bz2' then '5'
121
+ when '.gif' then 'g'
122
+ when '.jpg', '.png' then 'I'
123
+ when '.mp3', '.wav' then 's'
124
+ else '0'
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,10 @@
1
+ module Gopher
2
+ module Rendering
3
+
4
+ #
5
+ # class for text output
6
+ #
7
+ class Text < Base
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,21 @@
1
+ module Gopher
2
+
3
+ #
4
+ # basic class for an incoming request
5
+ #
6
+ class Request
7
+ attr_accessor :selector, :input, :ip_address
8
+
9
+ def initialize(raw, ip_addr=nil)
10
+ @selector, @input = raw.chomp.split("\t")
11
+ @ip_address = ip_addr
12
+ end
13
+
14
+ # confirm that this is actually a valid gopher request
15
+ # @return [Boolean] true if the request is valid, false otherwise
16
+ def valid?
17
+ # The Selector string should be no longer than 255 characters. (RFC 1436)
18
+ @selector.length <= 255
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ module Gopher
2
+
3
+ #
4
+ # basic class for server response to a request. contains the
5
+ # rendered results, a code that indicates success/failure, and can
6
+ # report the size of the response
7
+ #
8
+ class Response
9
+ attr_accessor :body
10
+ attr_accessor :code
11
+
12
+ #
13
+ # get the size, in bytes, of the response. used for logging
14
+ # @return [Integer] size
15
+ #
16
+ def size
17
+ case self.body
18
+ when String then self.body.length
19
+ when StringIO then self.body.length
20
+ when File then self.body.size
21
+ else 0
22
+ end
23
+ end
24
+ end
25
+ end