serverside 0.4.1 → 0.4.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|