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,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