gopher2000 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|