serverside 0.2.8 → 0.2.9
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/CHANGELOG +24 -0
- data/Rakefile +3 -8
- data/bin/serverside +1 -1
- data/lib/serverside/caching.rb +35 -12
- data/lib/serverside/controller.rb +67 -0
- data/lib/serverside/request.rb +10 -0
- data/lib/serverside/routing.rb +4 -4
- data/lib/serverside/static.rb +7 -1
- data/test/functional/static_server.rb +1 -1
- data/test/spec/caching_spec.rb +39 -1
- data/test/spec/controller_spec.rb +139 -0
- data/test/spec/request_spec.rb +288 -0
- data/test/unit/routing_test.rb +6 -6
- metadata +25 -22
data/CHANGELOG
CHANGED
@@ -1,3 +1,27 @@
|
|
1
|
+
==0.2.9
|
2
|
+
|
3
|
+
* Improved rake clean task.
|
4
|
+
|
5
|
+
* Added HTTP::Request.send_file method.
|
6
|
+
|
7
|
+
* Added basic rendering capabilities to ServerSide::Controller.
|
8
|
+
|
9
|
+
* Added support for lambda rules in ServerSide::Controller.mount.
|
10
|
+
|
11
|
+
* Start work on controller class.
|
12
|
+
|
13
|
+
* Renamed Router.route_default to default_route.
|
14
|
+
|
15
|
+
* Fixed small errors in documentation.
|
16
|
+
|
17
|
+
* Changed ServerSide::Static.serve_static to look for index.html inside directory before serving the directory.
|
18
|
+
|
19
|
+
* Added HTTP::Caching.cache_etags and cache_stamp methods.
|
20
|
+
|
21
|
+
* Fixed bug in doc_rforge rake task.
|
22
|
+
|
23
|
+
* Rewrote request unit test into request spec.
|
24
|
+
|
1
25
|
==0.2.8
|
2
26
|
|
3
27
|
* Refactored ServerSide::Static to use HTTP::Caching code.
|
data/Rakefile
CHANGED
@@ -7,8 +7,8 @@ require 'fileutils'
|
|
7
7
|
include FileUtils
|
8
8
|
|
9
9
|
NAME = "serverside"
|
10
|
-
VERS = "0.2.
|
11
|
-
CLEAN.include ['**/.*.sw?', '
|
10
|
+
VERS = "0.2.9"
|
11
|
+
CLEAN.include ['**/.*.sw?', 'pkg/*', '.config', 'doc/*', 'coverage/*']
|
12
12
|
RDOC_OPTS = ['--quiet', '--title', "ServerSide Documentation",
|
13
13
|
"--opname", "index.html",
|
14
14
|
"--line-numbers",
|
@@ -67,6 +67,7 @@ task :uninstall => [:clean] do
|
|
67
67
|
sh %{sudo gem uninstall #{NAME}}
|
68
68
|
end
|
69
69
|
|
70
|
+
desc 'Update docs and upload to rubyforge.org'
|
70
71
|
task :doc_rforge do
|
71
72
|
sh %{rake doc}
|
72
73
|
sh %{scp -r doc/rdoc/* ciconia@rubyforge.org:/var/www/gforge-projects/serverside}
|
@@ -124,12 +125,6 @@ RCov::VerifyTask.new(:rcov_verify => :rcov) do |t|
|
|
124
125
|
t.index_html = 'doc/output/coverage/index.html'
|
125
126
|
end
|
126
127
|
|
127
|
-
desc 'Update docs and upload to rubyforge.org'
|
128
|
-
task :doc_rforge do
|
129
|
-
sh %{rake doc}
|
130
|
-
sh "scp -C -r * ciconia@rubyforge.org:/var/www/gforge-projects/serverside/"
|
131
|
-
end
|
132
|
-
|
133
128
|
##############################################################################
|
134
129
|
# Statistics
|
135
130
|
##############################################################################
|
data/bin/serverside
CHANGED
data/lib/serverside/caching.rb
CHANGED
@@ -14,7 +14,30 @@ module ServerSide
|
|
14
14
|
NOT_MODIFIED_CLOSE = "HTTP/1.1 304 Not Modified\r\nDate: %s\r\nConnection: close\r\nLast-Modified: %s\r\nETag: \"%s\"\r\nCache-Control: max-age=%d\r\n\r\n".freeze
|
15
15
|
NOT_MODIFIED_PERSIST = "HTTP/1.1 304 Not Modified\r\nDate: %s\r\nLast-Modified: %s\r\nETag: \"%s\"\r\nCache-Control: max-age=%d\r\n\r\n".freeze
|
16
16
|
MAX_AGE = 'max-age=%d'.freeze
|
17
|
+
IF_NONE_MATCH_REGEXP = /^"?([^"]+)"?$/.freeze
|
17
18
|
|
19
|
+
# Returns an array containing all etags specified by the client in the
|
20
|
+
# If-None-Match header.
|
21
|
+
def cache_etags
|
22
|
+
h = @headers[IF_NONE_MATCH]
|
23
|
+
return [] unless h
|
24
|
+
h.split(',').inject([]) do |m, i|
|
25
|
+
i.strip =~ IF_NONE_MATCH_REGEXP ? (m << $1) : m
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the cache stamp specified by the client in the
|
30
|
+
# If-Modified-Since header. If no stamp is specified, returns nil.
|
31
|
+
def cache_stamp
|
32
|
+
(h = @headers[IF_MODIFIED_SINCE]) ? Time.httpdate(h) : nil
|
33
|
+
rescue
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
37
|
+
# Checks the request headers for validators and returns true if the
|
38
|
+
# client cache is valid. The validators can be either etags (specified
|
39
|
+
# in the If-None-Match header), or a modification stamp (specified in the
|
40
|
+
# If-Modified-Since header.)
|
18
41
|
def valid_client_cache?(etag, http_stamp)
|
19
42
|
none_match = @headers[IF_NONE_MATCH]
|
20
43
|
modified_since = @headers[IF_MODIFIED_SINCE]
|
@@ -22,6 +45,15 @@ module ServerSide
|
|
22
45
|
(modified_since && (modified_since == http_stamp))
|
23
46
|
end
|
24
47
|
|
48
|
+
# Validates the client cache by checking any supplied validators in the
|
49
|
+
# request. If the client cache is not valid, the specified block is
|
50
|
+
# executed. This method also makes sure the correct validators are
|
51
|
+
# included in the response - along with a Cache-Control header, to allow
|
52
|
+
# the client to cache the response. A possible usage:
|
53
|
+
#
|
54
|
+
# validate_cache("1234-5678", Time.now, 360) do
|
55
|
+
# send_response(200, "text/html", body)
|
56
|
+
# end
|
25
57
|
def validate_cache(etag, stamp, max_age = DEFAULT_MAX_AGE, &block)
|
26
58
|
http_stamp = stamp.httpdate
|
27
59
|
if valid_client_cache?(etag, http_stamp)
|
@@ -34,23 +66,14 @@ module ServerSide
|
|
34
66
|
end
|
35
67
|
end
|
36
68
|
|
69
|
+
# Sends a 304 HTTP response, along with etag and stamp validators, and a
|
70
|
+
# Cache-Control header.
|
37
71
|
def send_not_modified(etag, http_time, max_age = DEFAULT_MAX_AGE)
|
38
72
|
@socket << ((@persistent ? NOT_MODIFIED_PERSIST : NOT_MODIFIED_CLOSE) %
|
39
73
|
[Time.now.httpdate, http_time, etag, max_age])
|
40
74
|
end
|
41
|
-
end
|
75
|
+
end
|
42
76
|
end
|
43
77
|
end
|
44
78
|
|
45
79
|
__END__
|
46
|
-
|
47
|
-
Reality::Controller.new(:node_history) do
|
48
|
-
default_flavor :html
|
49
|
-
valid_method :get
|
50
|
-
|
51
|
-
def html
|
52
|
-
validate_cache(etag, http_time) {|cache_headers|
|
53
|
-
render_template(cache_headers)
|
54
|
-
}
|
55
|
-
end
|
56
|
-
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'routing')
|
2
|
+
|
3
|
+
module ServerSide
|
4
|
+
class Controller
|
5
|
+
def self.mount(rule = nil, &block)
|
6
|
+
rule ||= block
|
7
|
+
raise ArgumentError, "No routing rule specified." if rule.nil?
|
8
|
+
c = Class.new(self) {}
|
9
|
+
ServerSide::Router.route(rule) {c.new(self)}
|
10
|
+
c
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(request)
|
14
|
+
@request = request
|
15
|
+
@path = request.path
|
16
|
+
@parameters = request.parameters
|
17
|
+
process
|
18
|
+
render_default if not @rendered
|
19
|
+
end
|
20
|
+
|
21
|
+
def process
|
22
|
+
end
|
23
|
+
|
24
|
+
def render_default
|
25
|
+
@request.send_response(200, 'text/plain', 'no response.')
|
26
|
+
end
|
27
|
+
|
28
|
+
def render(body, content_type)
|
29
|
+
@request.send_response(200, content_type, body)
|
30
|
+
@rendered = true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
__END__
|
36
|
+
|
37
|
+
require 'rubygems'
|
38
|
+
require 'active_support/inflector'
|
39
|
+
require 'metaid'
|
40
|
+
|
41
|
+
class ActionController
|
42
|
+
def self.default_routing_rule
|
43
|
+
if name.split('::').last =~ /(.+)Controller$/
|
44
|
+
controller = Inflector.underscore($1)
|
45
|
+
{:path => ["/#{controller}/:action", "/#{controller}/:action/:id"]}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.inherited(c)
|
50
|
+
routing_rule = c.respond_to?(:routing_rule) ?
|
51
|
+
c.routing_rule : c.default_routing_rule
|
52
|
+
if routing_rule
|
53
|
+
ServerSide::Router.route(routing_rule) {c.new(self)}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.route(arg = nil, &block)
|
58
|
+
rule = arg || block
|
59
|
+
meta_def(:get_route) {rule}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class MyController < ActionController
|
64
|
+
route "hello"
|
65
|
+
end
|
66
|
+
|
67
|
+
p MyController.get_route
|
data/lib/serverside/request.rb
CHANGED
@@ -165,6 +165,16 @@ module ServerSide
|
|
165
165
|
@persistent = false
|
166
166
|
end
|
167
167
|
|
168
|
+
CONTENT_DISPOSITION = 'Content-Disposition'.freeze
|
169
|
+
CONTENT_DESCRIPTION = 'Content-Description'.freeze
|
170
|
+
|
171
|
+
def send_file(content, content_type, disposition = :inline, filename = nil, description = nil)
|
172
|
+
disposition = filename ? "#{disposition}; filename=#{filename}" : "#{disposition}"
|
173
|
+
@response_headers[CONTENT_DISPOSITION] = disposition
|
174
|
+
@response_headers[CONTENT_DESCRIPTION] = description if description
|
175
|
+
send_response(200, content_type, content)
|
176
|
+
end
|
177
|
+
|
168
178
|
# Send a redirect response.
|
169
179
|
def redirect(location, permanent = false)
|
170
180
|
@socket << (STATUS_REDIRECT %
|
data/lib/serverside/routing.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
require File.join(File.dirname(__FILE__), 'request')
|
2
2
|
|
3
3
|
module ServerSide
|
4
|
-
# The Router class
|
5
|
-
#
|
4
|
+
# The Router class is a subclass of HTTP::Request that can invoke
|
5
|
+
# different handlers based on rules that can be specified either by
|
6
6
|
# lambdas or by hashes that contain variable names corresponding to patterns.
|
7
7
|
#
|
8
8
|
# The simplest form of a routing rule specifies a path pattern:
|
@@ -19,7 +19,7 @@ module ServerSide
|
|
19
19
|
#
|
20
20
|
# ServerSide.route(lambda {@headers['Agent'] =~ /Moz/}) {serve_static('moz'/@path)}
|
21
21
|
#
|
22
|
-
# Routing rules are evaluated
|
22
|
+
# Routing rules are evaluated backwards, so the rules should be ordered
|
23
23
|
# from the general to the specific.
|
24
24
|
class Router < HTTP::Request
|
25
25
|
# Returns true if routes were defined.
|
@@ -116,7 +116,7 @@ module ServerSide
|
|
116
116
|
end
|
117
117
|
|
118
118
|
# Sets the default handler for incoming requests.
|
119
|
-
def self.
|
119
|
+
def self.default_route(&block)
|
120
120
|
define_method(:default_handler, &block)
|
121
121
|
compile_rules
|
122
122
|
end
|
data/lib/serverside/static.rb
CHANGED
@@ -76,7 +76,13 @@ module ServerSide
|
|
76
76
|
elsif serve_template(path)
|
77
77
|
return
|
78
78
|
elsif File.directory?(path)
|
79
|
-
|
79
|
+
if File.file?(path/'index.html')
|
80
|
+
serve_file(path/'index.html')
|
81
|
+
elsif File.file?(path/'index.rhtml')
|
82
|
+
serve_template(path/'index.rhtml')
|
83
|
+
else
|
84
|
+
serve_dir(path)
|
85
|
+
end
|
80
86
|
else
|
81
87
|
send_response(404, 'text', FILE_NOT_FOUND)
|
82
88
|
end
|
data/test/spec/caching_spec.rb
CHANGED
@@ -98,4 +98,42 @@ context "Caching::valid_client_cache?" do
|
|
98
98
|
r.headers['If-Modified-Since'] = t.httpdate
|
99
99
|
r.valid_client_cache?('eeee', t.httpdate).should_not_be false
|
100
100
|
end
|
101
|
-
end
|
101
|
+
end
|
102
|
+
|
103
|
+
context "Caching::cache_etags" do
|
104
|
+
specify "should return an empty array if no If-None-Match header is included" do
|
105
|
+
r = DummyRequest.new
|
106
|
+
r.cache_etags.should_be_a_kind_of Array
|
107
|
+
r.cache_etags.should_equal []
|
108
|
+
end
|
109
|
+
|
110
|
+
specify "should return all etags included in the If-None-Match header" do
|
111
|
+
r = DummyRequest.new
|
112
|
+
r.headers['If-None-Match'] = '*'
|
113
|
+
r.cache_etags.should_equal ['*']
|
114
|
+
r.headers['If-None-Match'] = '*, "XXX-YYY","AAA-BBB"'
|
115
|
+
r.cache_etags.should_equal ['*', 'XXX-YYY', 'AAA-BBB']
|
116
|
+
r.headers['If-None-Match'] = '"abcd-EFGH"'
|
117
|
+
r.cache_etags.should_equal ['abcd-EFGH']
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context "Caching::cache_stamps" do
|
122
|
+
specify "should return nil if no If-Modified-Since header is included" do
|
123
|
+
r = DummyRequest.new
|
124
|
+
r.cache_stamp.should_be_nil
|
125
|
+
end
|
126
|
+
|
127
|
+
specify "should return nil if no an invalid stamp is specified" do
|
128
|
+
r = DummyRequest.new
|
129
|
+
r.headers['If-Modified-Since'] = 'invalid stamp'
|
130
|
+
r.cache_stamp.should_be_nil
|
131
|
+
end
|
132
|
+
|
133
|
+
specify "should return the stamp specified in the If-Modified-Since header" do
|
134
|
+
r = DummyRequest.new
|
135
|
+
t = Time.now
|
136
|
+
r.headers['If-Modified-Since'] = t.httpdate
|
137
|
+
r.cache_stamp.to_i.should_equal t.to_i
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '../../lib/serverside')
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
class ServerSide::Router
|
5
|
+
def self.rules
|
6
|
+
@@rules
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.reset_rules
|
10
|
+
@@rules = []
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_accessor :path
|
14
|
+
end
|
15
|
+
|
16
|
+
context "ServerSide::Controller.mount" do
|
17
|
+
specify "should accept a routing rule as argument" do
|
18
|
+
proc {ServerSide::Controller.mount}.should_raise ArgumentError
|
19
|
+
end
|
20
|
+
|
21
|
+
specify "should return a subclass of ServerSide::Controller" do
|
22
|
+
c = ServerSide::Controller.mount(:path => '/test')
|
23
|
+
c.should_be_a_kind_of Class
|
24
|
+
c.superclass.should_be ServerSide::Controller
|
25
|
+
end
|
26
|
+
|
27
|
+
specify "should add a routing rule using ServerSide::Router.route" do
|
28
|
+
ServerSide::Router.reset_rules
|
29
|
+
rule = {:path => '/test'}
|
30
|
+
c = ServerSide::Controller.mount(rule)
|
31
|
+
r = ServerSide::Router.rules.first
|
32
|
+
r.first.should_equal rule
|
33
|
+
r.last.should_be_a_kind_of Proc
|
34
|
+
c.module_eval do
|
35
|
+
define_method(:initialize) {|req| $req = req}
|
36
|
+
end
|
37
|
+
res = r.last.call
|
38
|
+
res.should_be_a_kind_of c
|
39
|
+
|
40
|
+
r = ServerSide::Router.new(StringIO.new)
|
41
|
+
r.path = '/test'
|
42
|
+
r.respond
|
43
|
+
$req.should_be_a_kind_of ServerSide::Router
|
44
|
+
end
|
45
|
+
|
46
|
+
specify "should accept either an argument or block as the rule" do
|
47
|
+
ServerSide::Router.reset_rules
|
48
|
+
rule = {:path => '/test'}
|
49
|
+
c = ServerSide::Controller.mount(rule)
|
50
|
+
r = ServerSide::Router.rules.first
|
51
|
+
r.first.should_be rule
|
52
|
+
|
53
|
+
ServerSide::Router.reset_rules
|
54
|
+
rule = proc {true}
|
55
|
+
c = ServerSide::Controller.mount(&rule)
|
56
|
+
r = ServerSide::Router.rules.first
|
57
|
+
r.first.should_be rule
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class ServerSide::Controller
|
62
|
+
attr_accessor :request, :path, :parameters, :rendered
|
63
|
+
end
|
64
|
+
|
65
|
+
class ServerSide::HTTP::Request
|
66
|
+
attr_accessor :path, :parameters
|
67
|
+
end
|
68
|
+
|
69
|
+
require 'metaid'
|
70
|
+
|
71
|
+
class DummyController < ServerSide::Controller
|
72
|
+
attr_reader :process_called
|
73
|
+
|
74
|
+
def process
|
75
|
+
@process_called = true
|
76
|
+
end
|
77
|
+
|
78
|
+
def render_default
|
79
|
+
@rendered = :default
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context "ServerSide::Controller new instance" do
|
84
|
+
specify "should set @request, @path, and @parameters instance variables" do
|
85
|
+
req = ServerSide::HTTP::Request.new(StringIO.new)
|
86
|
+
req.path = '/aa/bb/cc'
|
87
|
+
req.parameters = {:q => 'node_state', :f => 'xml'}
|
88
|
+
c = ServerSide::Controller.new(req)
|
89
|
+
c.request.should_be req
|
90
|
+
c.path.should_be req.path
|
91
|
+
c.parameters.should_be req.parameters
|
92
|
+
end
|
93
|
+
|
94
|
+
specify "should invoke the process method" do
|
95
|
+
req = ServerSide::HTTP::Request.new(StringIO.new)
|
96
|
+
c = DummyController.new(req)
|
97
|
+
c.process_called.should_be true
|
98
|
+
end
|
99
|
+
|
100
|
+
specify "should invoke render_default unless @rendered" do
|
101
|
+
req = ServerSide::HTTP::Request.new(StringIO.new)
|
102
|
+
c = DummyController.new(req)
|
103
|
+
c.rendered.should_be :default
|
104
|
+
|
105
|
+
c_class = Class.new(DummyController) do
|
106
|
+
define_method(:process) {@rendered = true}
|
107
|
+
end
|
108
|
+
c = c_class.new(req)
|
109
|
+
c.rendered.should_be true
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
context "ServerSide::Controller.render_default" do
|
114
|
+
specify "should render a default 200 response" do
|
115
|
+
req = ServerSide::HTTP::Request.new(StringIO.new)
|
116
|
+
c = ServerSide::Controller.new(req)
|
117
|
+
req.socket.rewind
|
118
|
+
resp = req.socket.read
|
119
|
+
resp.should_match /HTTP\/1\.1\s200/
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context "ServerSide::Controller.render" do
|
124
|
+
specify "should render a 200 response with body and content type arguments" do
|
125
|
+
req = ServerSide::HTTP::Request.new(StringIO.new)
|
126
|
+
c = ServerSide::Controller.new(req)
|
127
|
+
c.render('hello world', 'text/plain')
|
128
|
+
req.socket.rewind
|
129
|
+
resp = req.socket.read
|
130
|
+
resp.should_match /Content-Type:\stext\/plain\r\n/
|
131
|
+
end
|
132
|
+
|
133
|
+
specify "should set @rendered to true" do
|
134
|
+
req = ServerSide::HTTP::Request.new(StringIO.new)
|
135
|
+
c = ServerSide::Controller.new(req)
|
136
|
+
c.render('hello world', 'text/plain')
|
137
|
+
c.rendered.should_equal true
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,288 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '../../lib/serverside')
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
class DummyRequest2 < ServerSide::HTTP::Request
|
5
|
+
attr_accessor :calls, :parse_result, :persistent
|
6
|
+
|
7
|
+
def parse
|
8
|
+
@calls ||= []
|
9
|
+
@calls << :parse
|
10
|
+
@parse_result
|
11
|
+
end
|
12
|
+
|
13
|
+
def respond
|
14
|
+
@calls ||= []
|
15
|
+
@calls << :respond
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context "HTTP::Request.process" do
|
20
|
+
specify "should call parse and and short-circuit if the result is nil" do
|
21
|
+
r = DummyRequest2.new(nil)
|
22
|
+
r.process.should_be_nil
|
23
|
+
r.calls.should_equal [:parse]
|
24
|
+
|
25
|
+
r.calls = []
|
26
|
+
r.parse_result = false
|
27
|
+
r.process.should_be false
|
28
|
+
r.calls.should_equal [:parse]
|
29
|
+
end
|
30
|
+
|
31
|
+
specify "should follow parse with respond and return @persistent" do
|
32
|
+
r = DummyRequest2.new(nil)
|
33
|
+
r.parse_result = true
|
34
|
+
r.process.should_be_nil
|
35
|
+
r.calls.should_equal [:parse, :respond]
|
36
|
+
|
37
|
+
r.calls = []
|
38
|
+
r.persistent = 'mau'
|
39
|
+
r.process.should_equal 'mau'
|
40
|
+
r.calls.should_equal [:parse, :respond]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class ServerSide::HTTP::Request
|
45
|
+
attr_writer :socket, :persistent, :response_headers
|
46
|
+
end
|
47
|
+
|
48
|
+
context "HTTP::Request.parse" do
|
49
|
+
specify "should return nil for invalid requests" do
|
50
|
+
r = ServerSide::HTTP::Request.new(nil)
|
51
|
+
r.socket = StringIO.new('POST /test')
|
52
|
+
r.parse.should_be_nil
|
53
|
+
r.socket = StringIO.new('invalid string')
|
54
|
+
r.parse.should_be_nil
|
55
|
+
r.socket = StringIO.new('GET HTTP/1.1')
|
56
|
+
r.parse.should_be_nil
|
57
|
+
r.socket = StringIO.new('GET /test http')
|
58
|
+
r.parse.should_be_nil
|
59
|
+
r.socket = StringIO.new('GET /test HTTP')
|
60
|
+
r.parse.should_be_nil
|
61
|
+
r.socket = StringIO.new('GET /test HTTP/')
|
62
|
+
r.parse.should_be_nil
|
63
|
+
r.socket = StringIO.new('POST /test HTTP/1.1')
|
64
|
+
r.parse.should_be_nil
|
65
|
+
end
|
66
|
+
|
67
|
+
specify "should parse valid requests and return request headers" do
|
68
|
+
r = ServerSide::HTTP::Request.new(nil)
|
69
|
+
r.socket = StringIO.new(
|
70
|
+
"POST /test HTTP/1.1\r\nContent-Type: text/html\r\n\r\n")
|
71
|
+
r.parse.should_be r.headers
|
72
|
+
r.method.should_equal :post
|
73
|
+
r.path.should_equal '/test'
|
74
|
+
r.query.should_be_nil
|
75
|
+
r.version.should_equal '1.1'
|
76
|
+
r.parameters.should_equal({})
|
77
|
+
r.headers.should_equal({'Content-Type' => 'text/html'})
|
78
|
+
r.cookies.should_equal({})
|
79
|
+
r.response_cookies.should_be_nil
|
80
|
+
r.persistent.should_equal true
|
81
|
+
end
|
82
|
+
|
83
|
+
specify "should correctly handle trailing slash in path" do
|
84
|
+
r = ServerSide::HTTP::Request.new(nil)
|
85
|
+
r.socket = StringIO.new("POST /test/asdf/qw/ HTTP/1.1\r\n\r\n")
|
86
|
+
r.parse.should_not_be_nil
|
87
|
+
r.path.should_equal '/test/asdf/qw'
|
88
|
+
|
89
|
+
r.socket = StringIO.new(
|
90
|
+
"POST /test/asdf/qw/?time=24%20hours HTTP/1.1\r\n\r\n")
|
91
|
+
r.parse.should_not_be_nil
|
92
|
+
r.path.should_equal '/test/asdf/qw'
|
93
|
+
end
|
94
|
+
|
95
|
+
specify "should parse URL-encoded parameters" do
|
96
|
+
r = ServerSide::HTTP::Request.new(nil)
|
97
|
+
r.socket = StringIO.new(
|
98
|
+
"POST /test?q=node_history&time=24%20hours HTTP/1.1\r\n\r\n")
|
99
|
+
r.parse.should_not_be_nil
|
100
|
+
r.parameters.size.should_equal 2
|
101
|
+
r.parameters[:time].should_equal '24 hours'
|
102
|
+
r.parameters[:q].should_equal 'node_history'
|
103
|
+
end
|
104
|
+
|
105
|
+
specify "should correctly parse the HTTP version" do
|
106
|
+
r = ServerSide::HTTP::Request.new(nil)
|
107
|
+
r.socket = StringIO.new(
|
108
|
+
"POST / HTTP/1.0\r\n\r\n")
|
109
|
+
r.parse.should_not_be_nil
|
110
|
+
r.version.should_equal '1.0'
|
111
|
+
r.socket = StringIO.new(
|
112
|
+
"POST / HTTP/3.2\r\n\r\n")
|
113
|
+
r.parse.should_not_be_nil
|
114
|
+
r.version.should_equal '3.2'
|
115
|
+
end
|
116
|
+
|
117
|
+
specify "should set @persistent correctly" do
|
118
|
+
r = ServerSide::HTTP::Request.new(nil)
|
119
|
+
r.socket = StringIO.new("POST / HTTP/1.0\r\n\r\n")
|
120
|
+
r.parse.should_not_be_nil
|
121
|
+
r.persistent.should_equal false
|
122
|
+
r.socket = StringIO.new("POST / HTTP/1.1\r\n\r\n")
|
123
|
+
r.parse.should_not_be_nil
|
124
|
+
r.persistent.should_equal true
|
125
|
+
r.socket = StringIO.new("POST / HTTP/0.6\r\n\r\n")
|
126
|
+
r.parse.should_not_be_nil
|
127
|
+
r.persistent.should_equal false
|
128
|
+
r.socket = StringIO.new("POST / HTTP/1.1\r\nConnection: close\r\n\r\n")
|
129
|
+
r.parse.should_not_be_nil
|
130
|
+
r.persistent.should_equal false
|
131
|
+
r.socket = StringIO.new("POST / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n")
|
132
|
+
r.parse.should_not_be_nil
|
133
|
+
r.persistent.should_equal true
|
134
|
+
end
|
135
|
+
|
136
|
+
specify "should parse cookies" do
|
137
|
+
r = ServerSide::HTTP::Request.new(nil)
|
138
|
+
r.socket = StringIO.new(
|
139
|
+
"POST / HTTP/1.0\r\nCookie: abc=1342; def=7%2f4\r\n\r\n")
|
140
|
+
r.parse.should_not_be_nil
|
141
|
+
r.headers['Cookie'].should_equal 'abc=1342; def=7%2f4'
|
142
|
+
r.cookies.size.should_equal 2
|
143
|
+
r.cookies[:abc].should_equal '1342'
|
144
|
+
r.cookies[:def].should_equal '7/4'
|
145
|
+
end
|
146
|
+
|
147
|
+
specify "should parse the post body" do
|
148
|
+
r = ServerSide::HTTP::Request.new(nil)
|
149
|
+
r.socket = StringIO.new(
|
150
|
+
"POST /?q=node_history HTTP/1.0\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 15\r\n\r\ntime=24%20hours")
|
151
|
+
r.parse.should_not_be_nil
|
152
|
+
r.parameters.size.should_equal 2
|
153
|
+
r.parameters[:q].should_equal 'node_history'
|
154
|
+
r.parameters[:time].should_equal '24 hours'
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
context "HTTP::Request.send_response" do
|
159
|
+
specify "should format a response with status and body" do
|
160
|
+
r = ServerSide::HTTP::Request.new(nil)
|
161
|
+
r.socket = StringIO.new
|
162
|
+
r.send_response(200, 'text', 'Hello there!')
|
163
|
+
r.socket.rewind
|
164
|
+
r.socket.read.should_equal "HTTP/1.1 200\r\nDate: #{Time.now.httpdate}\r\nConnection: close\r\nContent-Type: text\r\nContent-Length: 12\r\n\r\nHello there!"
|
165
|
+
end
|
166
|
+
|
167
|
+
specify "should format a response without connect-close when persistent" do
|
168
|
+
r = ServerSide::HTTP::Request.new(nil)
|
169
|
+
r.socket = StringIO.new
|
170
|
+
r.persistent = true
|
171
|
+
r.send_response(200, 'text', 'Hello there!')
|
172
|
+
r.socket.rewind
|
173
|
+
r.socket.read.should_equal "HTTP/1.1 200\r\nDate: #{Time.now.httpdate}\r\nContent-Type: text\r\nContent-Length: 12\r\n\r\nHello there!"
|
174
|
+
end
|
175
|
+
|
176
|
+
specify "should format a response without content-length for streaming response" do
|
177
|
+
r = ServerSide::HTTP::Request.new(nil)
|
178
|
+
r.socket = StringIO.new
|
179
|
+
r.persistent = true
|
180
|
+
r.send_response(200, 'text')
|
181
|
+
r.socket.rewind
|
182
|
+
r.socket.read.should_equal "HTTP/1.1 200\r\nDate: #{Time.now.httpdate}\r\nConnection: close\r\nContent-Type: text\r\n\r\n"
|
183
|
+
r.stream('hey there')
|
184
|
+
r.socket.rewind
|
185
|
+
r.socket.read.should_equal "HTTP/1.1 200\r\nDate: #{Time.now.httpdate}\r\nConnection: close\r\nContent-Type: text\r\n\r\nhey there"
|
186
|
+
end
|
187
|
+
|
188
|
+
specify "should include response_headers and headers in the response" do
|
189
|
+
r = ServerSide::HTTP::Request.new(nil)
|
190
|
+
r.socket = StringIO.new
|
191
|
+
r.persistent = true
|
192
|
+
r.response_headers['XXX'] = 'Test'
|
193
|
+
r.send_response(200, 'text')
|
194
|
+
r.socket.rewind
|
195
|
+
r.socket.read.should_equal "HTTP/1.1 200\r\nDate: #{Time.now.httpdate}\r\nConnection: close\r\nContent-Type: text\r\nXXX: Test\r\n\r\n"
|
196
|
+
|
197
|
+
r = ServerSide::HTTP::Request.new(nil)
|
198
|
+
r.socket = StringIO.new
|
199
|
+
r.persistent = true
|
200
|
+
r.send_response(200, 'text', nil, nil, {'YYY' => 'TTT'})
|
201
|
+
r.socket.rewind
|
202
|
+
r.socket.read.should_equal "HTTP/1.1 200\r\nDate: #{Time.now.httpdate}\r\nConnection: close\r\nContent-Type: text\r\nYYY: TTT\r\n\r\n"
|
203
|
+
end
|
204
|
+
|
205
|
+
specify "should set persistent to false if exception is raised" do
|
206
|
+
r = ServerSide::HTTP::Request.new(nil)
|
207
|
+
r.persistent = true
|
208
|
+
proc {r.send_response(200, 'text', 'Hello there!')}.should_not_raise
|
209
|
+
r.persistent.should_equal false
|
210
|
+
end
|
211
|
+
|
212
|
+
specify "should include cookies in the response" do
|
213
|
+
r = ServerSide::HTTP::Request.new(nil)
|
214
|
+
r.socket = StringIO.new
|
215
|
+
t = Time.now + 360
|
216
|
+
r.set_cookie(:session, "ABCDEFG", t)
|
217
|
+
r.response_cookies.should_equal "Set-Cookie: session=ABCDEFG; path=/; expires=#{t.rfc2822}\r\n"
|
218
|
+
r.send_response(200, 'text', 'Hi!')
|
219
|
+
r.socket.rewind
|
220
|
+
r.socket.read.should_equal "HTTP/1.1 200\r\nDate: #{Time.now.httpdate}\r\nConnection: close\r\nContent-Type: text\r\nSet-Cookie: session=ABCDEFG; path=/; expires=#{t.rfc2822}\r\nContent-Length: 3\r\n\r\nHi!"
|
221
|
+
|
222
|
+
r = ServerSide::HTTP::Request.new(nil)
|
223
|
+
r.socket = StringIO.new
|
224
|
+
r.delete_cookie(:session)
|
225
|
+
r.response_cookies.should_equal "Set-Cookie: session=; path=/; expires=#{Time.at(0).rfc2822}\r\n"
|
226
|
+
r.send_response(200, 'text', 'Hi!')
|
227
|
+
r.socket.rewind
|
228
|
+
r.socket.read.should_equal "HTTP/1.1 200\r\nDate: #{Time.now.httpdate}\r\nConnection: close\r\nContent-Type: text\r\nSet-Cookie: session=; path=/; expires=#{Time.at(0).rfc2822}\r\nContent-Length: 3\r\n\r\nHi!"
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
context "HTTP::Request.send_file" do
|
233
|
+
specify "should include a disposition header in the response" do
|
234
|
+
socket = StringIO.new
|
235
|
+
r = ServerSide::HTTP::Request.new(socket)
|
236
|
+
r.send_file('text', 'text/plain', :attachment)
|
237
|
+
socket.rewind
|
238
|
+
socket.read.should_match /Content-Disposition: attachment\r\n/
|
239
|
+
end
|
240
|
+
|
241
|
+
specify "should use :inline as default disposition" do
|
242
|
+
socket = StringIO.new
|
243
|
+
r = ServerSide::HTTP::Request.new(socket)
|
244
|
+
r.send_file('text', 'text/plain')
|
245
|
+
socket.rewind
|
246
|
+
socket.read.should_match /Content-Disposition: inline\r\n/
|
247
|
+
end
|
248
|
+
|
249
|
+
specify "should include filename parameter if specified" do
|
250
|
+
socket = StringIO.new
|
251
|
+
r = ServerSide::HTTP::Request.new(socket)
|
252
|
+
r.send_file('text', 'text/plain', :attachment, 'text.txt')
|
253
|
+
socket.rewind
|
254
|
+
socket.read.should_match /Content-Disposition: attachment; filename=text.txt\r\n/
|
255
|
+
end
|
256
|
+
|
257
|
+
specify "should include a description header if specified" do
|
258
|
+
socket = StringIO.new
|
259
|
+
r = ServerSide::HTTP::Request.new(socket)
|
260
|
+
r.send_file('text', 'text/plain', :attachment, 'text.txt')
|
261
|
+
socket.rewind
|
262
|
+
socket.read.should_not_match /Content-Description:\s/
|
263
|
+
|
264
|
+
socket = StringIO.new
|
265
|
+
r = ServerSide::HTTP::Request.new(socket)
|
266
|
+
r.send_file('text', 'text/plain', :attachment, 'text.txt', 'this is text.')
|
267
|
+
socket.rewind
|
268
|
+
socket.read.should_match /Content-Description:\sthis is text\./
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
context "HTTP::Request.redirect" do
|
273
|
+
specify "should send a 302 response for temporary redirect" do
|
274
|
+
r = ServerSide::HTTP::Request.new(nil)
|
275
|
+
r.socket = StringIO.new
|
276
|
+
r.redirect('http://mau.com/132')
|
277
|
+
r.socket.rewind
|
278
|
+
r.socket.read.should_equal "HTTP/1.1 302\r\nDate: #{Time.now.httpdate}\r\nConnection: close\r\nLocation: http://mau.com/132\r\n\r\n"
|
279
|
+
end
|
280
|
+
|
281
|
+
specify "should send a 301 response for permanent redirect" do
|
282
|
+
r = ServerSide::HTTP::Request.new(nil)
|
283
|
+
r.socket = StringIO.new
|
284
|
+
r.redirect('http://mau.com/132', true)
|
285
|
+
r.socket.rewind
|
286
|
+
r.socket.read.should_equal "HTTP/1.1 301\r\nDate: #{Time.now.httpdate}\r\nConnection: close\r\nLocation: http://mau.com/132\r\n\r\n"
|
287
|
+
end
|
288
|
+
end
|
data/test/unit/routing_test.rb
CHANGED
@@ -69,7 +69,7 @@ class RoutingTest < Test::Unit::TestCase
|
|
69
69
|
assert_equal nil, R.new(StringIO.new).respond
|
70
70
|
R.rules << [{:t => 'abc'}, lambda{1}]
|
71
71
|
R.rules << [{:t => 'def'}, lambda{2}]
|
72
|
-
R.
|
72
|
+
R.default_route {3}
|
73
73
|
R.compile_rules
|
74
74
|
r = R.new(StringIO.new)
|
75
75
|
r.t = 'abc'
|
@@ -148,11 +148,11 @@ class RoutingTest < Test::Unit::TestCase
|
|
148
148
|
assert_equal c, eval("R::#{tag}")
|
149
149
|
end
|
150
150
|
|
151
|
-
def
|
152
|
-
R.
|
151
|
+
def test_default_route
|
152
|
+
R.default_route {'mau m'}
|
153
153
|
assert_equal 'mau m', R.new(StringIO.new).default_handler
|
154
154
|
|
155
|
-
R.
|
155
|
+
R.default_route {654321}
|
156
156
|
assert_equal 654321, R.new(StringIO.new).default_handler
|
157
157
|
end
|
158
158
|
|
@@ -164,8 +164,8 @@ class RoutingTest < Test::Unit::TestCase
|
|
164
164
|
assert_equal 2, R.rules[0][1].call
|
165
165
|
end
|
166
166
|
|
167
|
-
def
|
168
|
-
ServerSide::Router.
|
167
|
+
def test_serverside_default_route
|
168
|
+
ServerSide::Router.default_route {1234}
|
169
169
|
assert_equal 1234, R.new(StringIO.new).default_handler
|
170
170
|
end
|
171
171
|
end
|
metadata
CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.0
|
|
3
3
|
specification_version: 1
|
4
4
|
name: serverside
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.2.
|
7
|
-
date: 2006-10-
|
6
|
+
version: 0.2.9
|
7
|
+
date: 2006-10-25 00:00:00 +02:00
|
8
8
|
summary: Performance-oriented web framework.
|
9
9
|
require_paths:
|
10
10
|
- lib
|
@@ -33,43 +33,46 @@ files:
|
|
33
33
|
- README
|
34
34
|
- Rakefile
|
35
35
|
- bin/serverside
|
36
|
+
- test/unit
|
36
37
|
- test/functional
|
37
38
|
- test/spec
|
38
39
|
- test/test_helper.rb
|
39
|
-
- test/unit
|
40
|
+
- test/unit/connection_test.rb
|
41
|
+
- test/unit/cluster_test.rb
|
42
|
+
- test/unit/daemon_test.rb
|
43
|
+
- test/unit/server_test.rb
|
44
|
+
- test/unit/static_test.rb
|
45
|
+
- test/unit/routing_test.rb
|
46
|
+
- test/unit/core_ext_test.rb
|
47
|
+
- test/unit/request_test.rb
|
48
|
+
- test/unit/template_test.rb
|
49
|
+
- test/functional/routing_server.rb
|
50
|
+
- test/functional/static_server.rb
|
40
51
|
- test/functional/primitive_static_server_test.rb
|
41
52
|
- test/functional/request_body_test.rb
|
42
|
-
- test/functional/routing_server.rb
|
43
|
-
- test/functional/routing_server_test.rb
|
44
53
|
- test/functional/static_profile.rb
|
45
54
|
- test/functional/static_rfuzz.rb
|
46
|
-
- test/functional/static_server.rb
|
47
55
|
- test/functional/static_server_test.rb
|
56
|
+
- test/functional/routing_server_test.rb
|
57
|
+
- test/spec/controller_spec.rb
|
58
|
+
- test/spec/core_ext_spec.rb
|
59
|
+
- test/spec/request_spec.rb
|
48
60
|
- test/spec/caching_spec.rb
|
49
61
|
- test/spec/connection_spec.rb
|
50
|
-
- test/spec/core_ext_spec.rb
|
51
|
-
- test/unit/cluster_test.rb
|
52
|
-
- test/unit/connection_test.rb
|
53
|
-
- test/unit/core_ext_test.rb
|
54
|
-
- test/unit/daemon_test.rb
|
55
|
-
- test/unit/request_test.rb
|
56
|
-
- test/unit/routing_test.rb
|
57
|
-
- test/unit/server_test.rb
|
58
|
-
- test/unit/static_test.rb
|
59
|
-
- test/unit/template_test.rb
|
60
62
|
- lib/serverside
|
61
63
|
- lib/serverside.rb
|
62
|
-
- lib/serverside/application.rb
|
63
|
-
- lib/serverside/caching.rb
|
64
|
-
- lib/serverside/cluster.rb
|
65
64
|
- lib/serverside/connection.rb
|
66
|
-
- lib/serverside/
|
65
|
+
- lib/serverside/cluster.rb
|
67
66
|
- lib/serverside/daemon.rb
|
68
|
-
- lib/serverside/
|
69
|
-
- lib/serverside/
|
67
|
+
- lib/serverside/application.rb
|
68
|
+
- lib/serverside/controller.rb
|
70
69
|
- lib/serverside/server.rb
|
71
70
|
- lib/serverside/static.rb
|
71
|
+
- lib/serverside/routing.rb
|
72
|
+
- lib/serverside/core_ext.rb
|
73
|
+
- lib/serverside/request.rb
|
72
74
|
- lib/serverside/template.rb
|
75
|
+
- lib/serverside/caching.rb
|
73
76
|
- CHANGELOG
|
74
77
|
test_files: []
|
75
78
|
|