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