serverside 0.4.1 → 0.4.3
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/README +0 -26
- data/Rakefile +10 -5
- data/bin/serverside +4 -3
- data/lib/serverside/core_ext.rb +5 -0
- data/lib/serverside/http/caching.rb +35 -27
- data/lib/serverside/http/const.rb +8 -2
- data/lib/serverside/http/error.rb +8 -3
- data/lib/serverside/http/parsing.rb +43 -61
- data/lib/serverside/http/request.rb +75 -0
- data/lib/serverside/http/response.rb +94 -62
- data/lib/serverside/http/server.rb +44 -68
- data/lib/serverside/http/static.rb +34 -20
- data/lib/serverside/http.rb +2 -1
- data/lib/serverside/js.rb +4 -100
- data/lib/serverside/xml.rb +4 -4
- data/spec/http_spec.rb +94 -92
- data/spec/js_spec.rb +86 -0
- data/spec/request_spec.rb +133 -0
- data/spec/response_spec.rb +287 -0
- data/spec/spec_helper.rb +39 -0
- data/spec/template_spec.rb +15 -17
- data/spec/xml_spec.rb +75 -0
- metadata +20 -15
data/README
CHANGED
@@ -38,32 +38,6 @@ To run the server without forking, use the 'serve' command:
|
|
38
38
|
|
39
39
|
serverside serve .
|
40
40
|
|
41
|
-
=== Serving ERb Templates
|
42
|
-
|
43
|
-
ServerSide can render ERb[http://www.ruby-doc.org/stdlib/libdoc/erb/rdoc/]
|
44
|
-
templates in a fashion similar to PHP. You can store templates in .rhtml files,
|
45
|
-
and ServerSide takes care of all the rest. ServerSide is also smart enough to
|
46
|
-
allow you to use nice looking URL's with your templates, and automatically adds
|
47
|
-
the .rhtml extension if the file is there.
|
48
|
-
|
49
|
-
=== Serving Dynamic Content
|
50
|
-
|
51
|
-
By default ServerSide serves static files, but you can change the behavior by
|
52
|
-
creating custom {routing rules}[classes/ServerSide/Connection/Router.html].
|
53
|
-
Here's a simple routing rule:
|
54
|
-
|
55
|
-
route :path => '/hello/:name' do
|
56
|
-
send_response(200, 'text', "Hello #{@params[:name]}")
|
57
|
-
end
|
58
|
-
|
59
|
-
The ServerSide framework also lets you route requests based on any attribute of
|
60
|
-
incoming requests, such as host name, path, URL parameters etc.
|
61
|
-
|
62
|
-
To run your custom rules, you can either put them in a file called serverside.rb,
|
63
|
-
or tell serverside to explicitly load a specific file:
|
64
|
-
|
65
|
-
serverside start ~/myapp/myapp.rb
|
66
|
-
|
67
41
|
=== Running a Cluster of Servers
|
68
42
|
|
69
43
|
ServerSide makes it easy to control a cluster of servers. Just supply a range of
|
data/Rakefile
CHANGED
@@ -6,7 +6,7 @@ require 'fileutils'
|
|
6
6
|
include FileUtils
|
7
7
|
|
8
8
|
NAME = "serverside"
|
9
|
-
VERS = "0.4.
|
9
|
+
VERS = "0.4.3"
|
10
10
|
CLEAN.include ['**/.*.sw?', 'pkg/*', '.config', 'doc/*', 'coverage/*']
|
11
11
|
RDOC_OPTS = ['--quiet', '--title', "ServerSide: a Fast Ruby Web Framework",
|
12
12
|
"--opname", "index.html",
|
@@ -64,6 +64,11 @@ task :install do
|
|
64
64
|
sh %{sudo gem install pkg/#{NAME}-#{VERS}}
|
65
65
|
end
|
66
66
|
|
67
|
+
task :install_no_docs do
|
68
|
+
sh %{rake package}
|
69
|
+
sh %{sudo gem install pkg/#{NAME}-#{VERS} --no-rdoc --no-ri}
|
70
|
+
end
|
71
|
+
|
67
72
|
task :uninstall => [:clean] do
|
68
73
|
sh %{sudo gem uninstall #{NAME}}
|
69
74
|
end
|
@@ -76,15 +81,15 @@ end
|
|
76
81
|
|
77
82
|
require 'spec/rake/spectask'
|
78
83
|
|
79
|
-
desc "Run specs"
|
84
|
+
desc "Run specs with coverage"
|
80
85
|
Spec::Rake::SpecTask.new('spec') do |t|
|
81
86
|
t.spec_files = FileList['spec/*_spec.rb']
|
87
|
+
t.rcov = true
|
82
88
|
end
|
83
89
|
|
84
|
-
desc "Run specs
|
85
|
-
Spec::Rake::SpecTask.new('
|
90
|
+
desc "Run specs without coverage"
|
91
|
+
Spec::Rake::SpecTask.new('spec_no_rcov') do |t|
|
86
92
|
t.spec_files = FileList['spec/*_spec.rb']
|
87
|
-
t.rcov = true
|
88
93
|
end
|
89
94
|
|
90
95
|
##############################################################################
|
data/bin/serverside
CHANGED
@@ -64,13 +64,14 @@ elsif File.file?($path/'serverside.rb')
|
|
64
64
|
app_code = IO.read($path/'serverside.rb')
|
65
65
|
end
|
66
66
|
|
67
|
+
ServerSide::HTTP::Static.static_root = File.expand_path($path)
|
68
|
+
|
67
69
|
$server = ServerSide::HTTP::Server.new do
|
68
70
|
if app_code
|
69
71
|
module_eval(app_code)
|
70
72
|
else
|
71
|
-
|
72
|
-
|
73
|
-
serve_static($path/@uri)
|
73
|
+
def handle(req)
|
74
|
+
ServerSide::HTTP::Response.static(req.path)
|
74
75
|
end
|
75
76
|
end
|
76
77
|
end
|
data/lib/serverside/core_ext.rb
CHANGED
@@ -1,42 +1,45 @@
|
|
1
1
|
module ServerSide::HTTP
|
2
2
|
# HTTP Caching behavior
|
3
3
|
module Caching
|
4
|
-
# Sets caching-related headers
|
5
|
-
# If-None-Match headers. If a match is found, a 304 response is sent.
|
6
|
-
# Otherwise, the supplied block is invoked.
|
4
|
+
# Sets caching-related headers (Cache-Control and Expires).
|
7
5
|
def cache(opts)
|
8
|
-
|
6
|
+
# check cache-control
|
7
|
+
remove_cache_control
|
8
|
+
if cache_control = opts[:cache_control]
|
9
|
+
add_header(CACHE_CONTROL, cache_control)
|
10
|
+
end
|
11
|
+
|
12
|
+
# add an expires header
|
13
|
+
if expires = opts[:expires] || (opts[:ttl] && (Time.now + opts[:ttl]))
|
14
|
+
add_header(EXPIRES, expires.httpdate)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Validates the supplied request against specified validators (etag and
|
19
|
+
# last-modified stamp). If a match is found, the status is changed to
|
20
|
+
# 304 Not Modified. Otherwise, the supplied block is invoked.
|
21
|
+
def validate_cache(opts, &block)
|
22
|
+
valid_cache = false
|
9
23
|
|
10
24
|
# check etag
|
11
25
|
if etag = opts[:etag]
|
12
26
|
etag = "\"#{etag}\""
|
13
27
|
add_header(ETAG, etag) if etag
|
14
|
-
|
28
|
+
valid_cache = etag_match(etag)
|
15
29
|
end
|
16
30
|
|
17
31
|
# check last_modified
|
18
32
|
if last_modified = opts[:last_modified]
|
19
|
-
add_header(LAST_MODIFIED, last_modified)
|
20
|
-
|
33
|
+
add_header(LAST_MODIFIED, last_modified.httpdate)
|
34
|
+
valid_cache ||= modified_match(last_modified)
|
21
35
|
end
|
22
36
|
|
23
|
-
#
|
24
|
-
|
25
|
-
if cache_control = opts[:cache_control]
|
26
|
-
add_header(CACHE_CONTROL, cache_control)
|
27
|
-
end
|
37
|
+
# set cache-related headers
|
38
|
+
cache(opts)
|
28
39
|
|
29
|
-
#
|
30
|
-
if expires = opts[:expires]
|
31
|
-
add_header(EXPIRES, expires.httpdate)
|
32
|
-
elsif age = opts[:age]
|
33
|
-
add_header(EXPIRES, (Time.now + age).httpdate)
|
34
|
-
end
|
35
|
-
|
36
|
-
# if not modified, send a 304 response. Otherwise we yield to the
|
40
|
+
# if not modified, we have a 304 response. Otherwise we yield to the
|
37
41
|
# supplied block.
|
38
|
-
|
39
|
-
send_response(STATUS_NOT_MODIFIED, nil) : yield
|
42
|
+
valid_cache ? (@status = STATUS_NOT_MODIFIED) : yield(self)
|
40
43
|
end
|
41
44
|
|
42
45
|
COMMA = ','.freeze
|
@@ -44,10 +47,10 @@ module ServerSide::HTTP
|
|
44
47
|
# Matches the supplied etag against any of the entities in the
|
45
48
|
# If-None-Match header.
|
46
49
|
def etag_match(etag)
|
47
|
-
|
50
|
+
return false unless @request
|
51
|
+
matches = @request.headers[IF_NONE_MATCH]
|
48
52
|
if matches
|
49
53
|
matches.split(COMMA).each do |e|
|
50
|
-
|
51
54
|
return true if e.strip == etag
|
52
55
|
end
|
53
56
|
end
|
@@ -57,13 +60,14 @@ module ServerSide::HTTP
|
|
57
60
|
# Matches the supplied last modified date against the If-Modified-Since
|
58
61
|
# header.
|
59
62
|
def modified_match(last_modified)
|
60
|
-
|
63
|
+
return false unless @request
|
64
|
+
if modified_since = @request.headers[IF_MODIFIED_SINCE]
|
61
65
|
last_modified.to_i == Time.parse(modified_since).to_i
|
62
66
|
else
|
63
67
|
false
|
64
68
|
end
|
65
69
|
rescue => e
|
66
|
-
raise
|
70
|
+
raise BadRequestError, "Invalid value in If-Modified-Since header"
|
67
71
|
end
|
68
72
|
|
69
73
|
# Sets the Cache-Control header.
|
@@ -71,8 +75,12 @@ module ServerSide::HTTP
|
|
71
75
|
add_header(CACHE_CONTROL, directive)
|
72
76
|
end
|
73
77
|
|
78
|
+
def set_no_cache
|
79
|
+
set_cache_control(NO_CACHE)
|
80
|
+
end
|
81
|
+
|
74
82
|
def remove_cache_control
|
75
|
-
@
|
83
|
+
@headers.reject! {|h| h =~ /^#{CACHE_CONTROL}/}
|
76
84
|
end
|
77
85
|
end
|
78
86
|
end
|
@@ -4,9 +4,13 @@ module ServerSide::HTTP
|
|
4
4
|
VERSION_1_1 = '1.1'.freeze
|
5
5
|
|
6
6
|
# maximum sizes
|
7
|
-
|
8
|
-
|
7
|
+
# compare to http://mongrel.rubyforge.org/security.html
|
8
|
+
MAX_REQUEST_LINE_SIZE = 1024
|
9
|
+
MAX_HEADER_SIZE = 112 * 1024 # 112KB
|
10
|
+
MAX_HEADER_NAME_SIZE = 256
|
9
11
|
MAX_HEADER_COUNT = 256 # should be enough methinks
|
12
|
+
MAX_PARAMETER_VALUE_SIZE = 10240 # 10KB
|
13
|
+
MAX_PARAMETER_NAME_SIZE = 64 # should be enough
|
10
14
|
|
11
15
|
# request body and response body
|
12
16
|
CONTENT_LENGTH = 'Content-Length'.freeze
|
@@ -27,6 +31,8 @@ module ServerSide::HTTP
|
|
27
31
|
X_FORWARDED_FOR = 'X-Forwarded-For'.freeze
|
28
32
|
DATE = 'Date'.freeze
|
29
33
|
LOCATION = 'Location'.freeze
|
34
|
+
ACCEPT = 'Accept'.freeze
|
35
|
+
USER_AGENT = 'User-Agent'.freeze
|
30
36
|
|
31
37
|
# caching
|
32
38
|
IF_NONE_MATCH = 'If-None-Match'.freeze
|
@@ -13,12 +13,17 @@ end
|
|
13
13
|
|
14
14
|
module ServerSide::HTTP
|
15
15
|
# This error is raised when a malformed request is encountered.
|
16
|
-
class
|
16
|
+
class BadRequestError < RuntimeError
|
17
17
|
set_http_status STATUS_BAD_REQUEST
|
18
18
|
end
|
19
19
|
|
20
|
-
# This error is raised when an invalid
|
21
|
-
class
|
20
|
+
# This error is raised when an invalid resource is referenced.
|
21
|
+
class NotFoundError < RuntimeError
|
22
22
|
set_http_status STATUS_NOT_FOUND
|
23
23
|
end
|
24
|
+
|
25
|
+
# This error is raised when access is not allowed.
|
26
|
+
class ForbiddenError < RuntimeError
|
27
|
+
set_http_status STATUS_FORBIDDEN
|
28
|
+
end
|
24
29
|
end
|
@@ -7,28 +7,46 @@ module ServerSide::HTTP
|
|
7
7
|
def parse_request_line(line)
|
8
8
|
if line =~ REQUEST_LINE_RE
|
9
9
|
@request_line = line
|
10
|
-
@method, @
|
10
|
+
@method, @path, @query, @http_version = $1.downcase.to_sym, $2, $3, $4
|
11
11
|
@params = @query ? parse_query_parameters(@query) : {}
|
12
|
+
|
13
|
+
# HTTP 1.1 connections are persistent by default.
|
14
|
+
@persistent = @http_version == VERSION_1_1
|
12
15
|
else
|
13
|
-
raise
|
16
|
+
raise BadRequestError, "Invalid request format"
|
14
17
|
end
|
15
18
|
end
|
16
19
|
|
17
20
|
HEADER_RE = /([^:]+):\s*(.*)/.freeze
|
18
21
|
|
22
|
+
HYPHEN = '-'.freeze
|
23
|
+
UNDERSCORE = '_'.freeze
|
24
|
+
|
25
|
+
def header_to_sym(h)
|
26
|
+
h.downcase.gsub(HYPHEN, UNDERSCORE).to_sym
|
27
|
+
end
|
28
|
+
|
19
29
|
# Parses an HTTP header.
|
20
30
|
def parse_header(line)
|
31
|
+
# check header count
|
32
|
+
if (@header_count += 1) > MAX_HEADER_COUNT
|
33
|
+
raise BadRequestError, "Too many headers"
|
34
|
+
end
|
35
|
+
|
21
36
|
if line =~ HEADER_RE
|
22
|
-
|
37
|
+
if $1.size > MAX_HEADER_NAME_SIZE
|
38
|
+
raise BadRequestError, "Invalid header size"
|
39
|
+
end
|
40
|
+
k = header_to_sym($1)
|
23
41
|
v = $2.freeze
|
24
42
|
case k
|
25
|
-
when
|
26
|
-
when
|
27
|
-
when
|
43
|
+
when :content_length: @content_length = v.to_i
|
44
|
+
when :connection: @persistent = v == KEEP_ALIVE
|
45
|
+
when :cookie: parse_cookies(v)
|
28
46
|
end
|
29
|
-
@
|
47
|
+
@headers[k] = v
|
30
48
|
else
|
31
|
-
raise
|
49
|
+
raise BadRequestError, "Invalid header format"
|
32
50
|
end
|
33
51
|
end
|
34
52
|
|
@@ -40,9 +58,17 @@ module ServerSide::HTTP
|
|
40
58
|
def parse_query_parameters(query)
|
41
59
|
query.split(AMPERSAND).inject({}) do |m, i|
|
42
60
|
if i =~ PARAMETER_RE
|
43
|
-
|
61
|
+
k = $1
|
62
|
+
if k.size > MAX_PARAMETER_NAME_SIZE
|
63
|
+
raise BadRequestError, "Invalid parameter size"
|
64
|
+
end
|
65
|
+
v = $2
|
66
|
+
if v.size > MAX_PARAMETER_VALUE_SIZE
|
67
|
+
raise BadRequestError, "Invalid parameter size"
|
68
|
+
end
|
69
|
+
m[k.to_sym] = v.uri_unescape
|
44
70
|
else
|
45
|
-
raise
|
71
|
+
raise BadRequestError, "Invalid parameter format"
|
46
72
|
end
|
47
73
|
m
|
48
74
|
end
|
@@ -55,9 +81,9 @@ module ServerSide::HTTP
|
|
55
81
|
def parse_cookies(cookies)
|
56
82
|
cookies.split(SEMICOLON).each do |c|
|
57
83
|
if c.strip =~ COOKIE_RE
|
58
|
-
@
|
84
|
+
@cookies[$1.to_sym] = $2.uri_unescape
|
59
85
|
else
|
60
|
-
raise
|
86
|
+
raise BadRequestError, "Invalid cookie format"
|
61
87
|
end
|
62
88
|
end
|
63
89
|
end
|
@@ -65,8 +91,9 @@ module ServerSide::HTTP
|
|
65
91
|
BOUNDARY_FIX = '--'.freeze
|
66
92
|
|
67
93
|
# Parses the request body.
|
68
|
-
def
|
69
|
-
|
94
|
+
def parse_body(body)
|
95
|
+
@body = body
|
96
|
+
case @headers[:content_type]
|
70
97
|
when MULTIPART_FORM_DATA_RE:
|
71
98
|
parse_multi_part(body, BOUNDARY_FIX + $1) # body.dup so we keep the original request body?
|
72
99
|
when FORM_URL_ENCODED:
|
@@ -106,12 +133,12 @@ module ServerSide::HTTP
|
|
106
133
|
file_type = v
|
107
134
|
end
|
108
135
|
else
|
109
|
-
raise
|
136
|
+
raise BadRequestError, "Invalid header in part"
|
110
137
|
end
|
111
138
|
end
|
112
139
|
# check if we got the content name
|
113
140
|
unless part_name
|
114
|
-
raise
|
141
|
+
raise BadRequestError, "Invalid part content"
|
115
142
|
end
|
116
143
|
# part body
|
117
144
|
part_body = part.chomp! # what's left of it
|
@@ -126,50 +153,5 @@ module ServerSide::HTTP
|
|
126
153
|
@params ||= {}
|
127
154
|
@params.merge!(parse_query_parameters(body))
|
128
155
|
end
|
129
|
-
|
130
|
-
# Returns the host specified in the Host header.
|
131
|
-
def host
|
132
|
-
parse_host_header unless @host_header_parsed
|
133
|
-
@host
|
134
|
-
end
|
135
|
-
|
136
|
-
# Returns the port number if specified in the Host header.
|
137
|
-
def port
|
138
|
-
parse_host_header unless @host_header_parsed
|
139
|
-
@port
|
140
|
-
end
|
141
|
-
|
142
|
-
HOST_PORT_RE = /^([^:]+):(.+)$/.freeze
|
143
|
-
|
144
|
-
# Parses the Host header.
|
145
|
-
def parse_host_header
|
146
|
-
h = @request_headers[HOST]
|
147
|
-
if h =~ HOST_PORT_RE
|
148
|
-
@host = $1
|
149
|
-
@port = $2.to_i
|
150
|
-
else
|
151
|
-
@host = h
|
152
|
-
end
|
153
|
-
@host_header_parsed = true
|
154
|
-
end
|
155
|
-
|
156
|
-
# Returns the client name. The client name is either the value of the
|
157
|
-
# X-Forwarded-For header, or the result of get_peername.
|
158
|
-
def client_name
|
159
|
-
unless @client_name
|
160
|
-
@client_name = @request_headers[X_FORWARDED_FOR]
|
161
|
-
unless @client_name
|
162
|
-
if addr = get_peername
|
163
|
-
p, @client_name = Socket.unpack_sockaddr_in(addr)
|
164
|
-
end
|
165
|
-
end
|
166
|
-
end
|
167
|
-
@client_name
|
168
|
-
end
|
169
|
-
|
170
|
-
# Returns the request content type.
|
171
|
-
def content_type
|
172
|
-
@content_type ||= @request_headers[CONTENT_TYPE]
|
173
|
-
end
|
174
156
|
end
|
175
157
|
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module ServerSide::HTTP
|
2
|
+
class Request
|
3
|
+
include ServerSide::HTTP::Parsing
|
4
|
+
|
5
|
+
attr_reader :request_line, :method, :path, :query, :http_version, :params
|
6
|
+
attr_reader :content_length, :headers, :header_count, :persistent
|
7
|
+
attr_reader :cookies, :body
|
8
|
+
|
9
|
+
def initialize(conn)
|
10
|
+
@conn = conn
|
11
|
+
@headers = {}
|
12
|
+
@header_count = 0
|
13
|
+
@cookies = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the host specified in the Host header.
|
17
|
+
def host
|
18
|
+
parse_host_header unless @host_header_parsed
|
19
|
+
@host
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns the port number if specified in the Host header.
|
23
|
+
def port
|
24
|
+
parse_host_header unless @host_header_parsed
|
25
|
+
@port
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns true if the request was received on port 443
|
29
|
+
def encrypted?
|
30
|
+
port == 443
|
31
|
+
end
|
32
|
+
|
33
|
+
HOST_PORT_RE = /^([^:]+):(.+)$/.freeze
|
34
|
+
|
35
|
+
# Parses the Host header.
|
36
|
+
def parse_host_header
|
37
|
+
h = @headers[:host]
|
38
|
+
if h =~ HOST_PORT_RE
|
39
|
+
@host = $1
|
40
|
+
@port = $2.to_i
|
41
|
+
else
|
42
|
+
@host = h
|
43
|
+
end
|
44
|
+
@host_header_parsed = true
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns the client name. The client name is either the value of the
|
48
|
+
# X-Forwarded-For header, or the result of get_peername.
|
49
|
+
def client_name
|
50
|
+
unless @client_name
|
51
|
+
@client_name = @headers[:x_forwarded_for]
|
52
|
+
unless @client_name
|
53
|
+
if addr = @conn.get_peername
|
54
|
+
p, @client_name = Socket.unpack_sockaddr_in(addr)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
@client_name
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns true if the accept header contains the supplied pattern.
|
62
|
+
def accept?(re)
|
63
|
+
re = Regexp.new(re) unless Regexp === re
|
64
|
+
(h = @headers[:accept]) && (h =~ re) && true
|
65
|
+
end
|
66
|
+
|
67
|
+
def user_agent
|
68
|
+
@headers[:user_agent]
|
69
|
+
end
|
70
|
+
|
71
|
+
def content_type
|
72
|
+
@headers[:content_type]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -1,91 +1,123 @@
|
|
1
1
|
module ServerSide::HTTP
|
2
|
-
|
2
|
+
class Response
|
3
|
+
include Caching
|
4
|
+
include Static
|
5
|
+
|
6
|
+
attr_accessor :status, :body, :request
|
7
|
+
attr_reader :headers, :stream_period, :stream_proc
|
8
|
+
|
9
|
+
def initialize(opts = nil)
|
10
|
+
@status = STATUS_OK
|
11
|
+
@headers = []
|
12
|
+
@body = ''
|
13
|
+
if opts
|
14
|
+
opts.each do |k, v|
|
15
|
+
case k
|
16
|
+
when :status: @status = v
|
17
|
+
when :body: @body = v
|
18
|
+
when :close: @close = v
|
19
|
+
when :request: @request = v
|
20
|
+
when :static: serve_static(v)
|
21
|
+
else add_header(k, v)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def disable_response
|
28
|
+
@disable_response = true
|
29
|
+
end
|
30
|
+
|
31
|
+
def enable_response
|
32
|
+
@disable_response = false
|
33
|
+
end
|
34
|
+
|
35
|
+
def persistent?
|
36
|
+
!@close && !@stream_proc && @body
|
37
|
+
end
|
38
|
+
|
3
39
|
# Adds a header to the response.
|
4
40
|
def add_header(k, v)
|
5
|
-
|
41
|
+
k = k.to_header_name if (k.class == Symbol)
|
42
|
+
@headers << "#{k}: #{v}\r\n"
|
6
43
|
end
|
7
44
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
45
|
+
ROOT_PATH = '/'.freeze
|
46
|
+
|
47
|
+
# Adds a cookie to the response headers.
|
48
|
+
def set_cookie(name, value, opts = {})
|
49
|
+
path = opts[:path] || ROOT_PATH
|
50
|
+
expires = opts[:expires] || (opts[:ttl] && (Time.now + opts[:ttl])) || \
|
51
|
+
(Time.now + 86400) # if no expiry is specified we assume one day
|
52
|
+
|
53
|
+
v = "#{name}=#{value.to_s.uri_escape}; path=#{path}; expires=#{expires.httpdate}"
|
54
|
+
if domain = opts[:domain]
|
55
|
+
v << "; domain=#{domain}"
|
56
|
+
end
|
57
|
+
add_header(SET_COOKIE, v)
|
12
58
|
end
|
13
59
|
|
14
|
-
#
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
60
|
+
# Adds an expired cookie to the response headers.
|
61
|
+
def delete_cookie(name, opts = {})
|
62
|
+
set_cookie(name, nil, opts.merge(:expires => COOKIE_EXPIRED_TIME))
|
63
|
+
end
|
64
|
+
|
65
|
+
EMPTY = ''.freeze
|
66
|
+
|
67
|
+
def to_s
|
68
|
+
if @disable_response
|
69
|
+
EMPTY
|
20
70
|
else
|
21
|
-
|
71
|
+
if !streaming? && (content_length = @body && @body.size)
|
72
|
+
add_header(CONTENT_LENGTH, content_length)
|
73
|
+
end
|
74
|
+
"HTTP/1.1 #{@status}\r\nDate: #{Time.now.httpdate}\r\n#{@headers.join}\r\n#{@body}"
|
22
75
|
end
|
23
76
|
end
|
24
77
|
|
25
|
-
def
|
26
|
-
|
27
|
-
|
78
|
+
def stream(period, &block)
|
79
|
+
@stream_period = period
|
80
|
+
@stream_proc = block
|
81
|
+
self
|
28
82
|
end
|
29
83
|
|
30
|
-
|
31
|
-
|
32
|
-
def send_error_response(e)
|
33
|
-
send_response(e.http_status, e.message)
|
84
|
+
def streaming?
|
85
|
+
@stream_proc
|
34
86
|
end
|
35
87
|
|
36
|
-
|
37
|
-
|
38
|
-
# if the connection is to be closed, we add the Connection: close header.
|
39
|
-
# prepare date and other headers
|
40
|
-
add_header(DATE, Time.now.httpdate)
|
41
|
-
if (content_length ||= body && body.size)
|
42
|
-
add_header(CONTENT_LENGTH, content_length)
|
43
|
-
else
|
44
|
-
@persistent = false
|
45
|
-
end
|
46
|
-
unless @persistent
|
47
|
-
@response_headers << CONNECTION_CLOSE
|
48
|
-
end
|
49
|
-
@response_sent = true
|
50
|
-
send_data "HTTP/1.1 #{status}\r\n#{@response_headers.join}\r\n#{body}"
|
88
|
+
def self.blank
|
89
|
+
new(:body => nil)
|
51
90
|
end
|
52
91
|
|
53
|
-
def redirect(location,
|
54
|
-
|
55
|
-
send_response(permanent ? STATUS_MOVED_PERMANENTLY : STATUS_FOUND,'')
|
92
|
+
def self.redirect(location, status = STATUS_FOUND)
|
93
|
+
new(:status => status, :location => location)
|
56
94
|
end
|
57
95
|
|
58
|
-
|
59
|
-
|
60
|
-
@streaming = true
|
61
|
-
if content_type
|
62
|
-
add_header(CONTENT_TYPE, content_type)
|
63
|
-
end
|
64
|
-
send_response(status)
|
65
|
-
if body
|
66
|
-
send_data(body)
|
67
|
-
end
|
96
|
+
def self.static(path, options = {})
|
97
|
+
new(options.merge(:static => path))
|
68
98
|
end
|
69
99
|
|
70
|
-
def
|
71
|
-
|
100
|
+
def self.error(e)
|
101
|
+
new(:status => e.http_status, :close => true,
|
102
|
+
:body => "#{e.message}\r\n#{e.backtrace.join("\r\n")}")
|
72
103
|
end
|
73
104
|
|
74
|
-
|
105
|
+
def self.streaming(opts = nil, &block)
|
106
|
+
new(opts).stream(1, &block)
|
107
|
+
end
|
75
108
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
v = "#{name}=#{value.to_s.uri_escape}; path=#{path}; expires=#{expires.rfc2822}"
|
80
|
-
if domain
|
81
|
-
v << "; domain=#{domain}"
|
82
|
-
end
|
83
|
-
add_header(SET_COOKIE, v)
|
109
|
+
def set_representation(body, content_type)
|
110
|
+
@body = body
|
111
|
+
add_header(CONTENT_TYPE, content_type)
|
84
112
|
end
|
85
113
|
|
86
|
-
|
87
|
-
|
88
|
-
|
114
|
+
def redirect(location, status = STATUS_FOUND)
|
115
|
+
@status = status
|
116
|
+
add_header(:location, location)
|
117
|
+
end
|
118
|
+
|
119
|
+
def content_type=(v)
|
120
|
+
add_header(CONTENT_TYPE, v)
|
89
121
|
end
|
90
122
|
end
|
91
123
|
end
|