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 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.1"
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 with coverage"
85
- Spec::Rake::SpecTask.new('spec_coverage') do |t|
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
- include ServerSide::HTTP::Static
72
- def handle
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
@@ -67,6 +67,11 @@ class Symbol
67
67
  def /(o)
68
68
  File.join(self, o.to_s)
69
69
  end
70
+
71
+ # Converts a symbol into an HTTP header name
72
+ def to_header_name
73
+ to_s.split('_').map {|p| p.capitalize}.join('-')
74
+ end
70
75
  end
71
76
 
72
77
  class Proc
@@ -1,42 +1,45 @@
1
1
  module ServerSide::HTTP
2
2
  # HTTP Caching behavior
3
3
  module Caching
4
- # Sets caching-related headers and validates If-Modified-Since and
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
- not_modified = false
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
- not_modified = etag_match(etag)
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
- not_modified ||= modified_match(last_modified)
33
+ add_header(LAST_MODIFIED, last_modified.httpdate)
34
+ valid_cache ||= modified_match(last_modified)
21
35
  end
22
36
 
23
- # check cache-control
24
- remove_cache_control
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
- # add an expires header
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
- not_modified ?
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
- matches = @request_headers[IF_NONE_MATCH]
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
- if modified_since = @request_headers[IF_MODIFIED_SINCE]
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 MalformedRequestError, "Invalid value in If-Modified-Since header"
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
- @response_headers.reject! {|h| h =~ /^#{CACHE_CONTROL}/}
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
- MAX_REQUEST_LINE_SIZE = 8192
8
- MAX_HEADER_SIZE = 8192
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 MalformedRequestError < RuntimeError
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 file is referenced.
21
- class FileNotFoundError < RuntimeError
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, @uri, @query, @http_version = $1.downcase.to_sym, $2, $3, $4
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 MalformedRequestError, "Invalid request format"
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
- k = $1.freeze
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 CONTENT_LENGTH: @content_length = v.to_i
26
- when CONNECTION: @persistent = v == KEEP_ALIVE
27
- when COOKIE: parse_cookies(v)
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
- @request_headers[k] = v
47
+ @headers[k] = v
30
48
  else
31
- raise MalformedRequestError, "Invalid header format"
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
- m[$1.to_sym] = $2.uri_unescape
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 MalformedRequestError, "Invalid parameter format"
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
- @request_cookies[$1.to_sym] = $2.uri_unescape
84
+ @cookies[$1.to_sym] = $2.uri_unescape
59
85
  else
60
- raise MalformedRequestError, "Invalid cookie format"
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 parse_request_body(body)
69
- case @request_headers[CONTENT_TYPE]
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 MalformedRequestError, "Invalid header in part"
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 MalformedRequestError, "Invalid part content"
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
- module Response
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
- @response_headers << "#{k}: #{v}\r\n"
41
+ k = k.to_header_name if (k.class == Symbol)
42
+ @headers << "#{k}: #{v}\r\n"
6
43
  end
7
44
 
8
- # Sends a representation with a content type and a body.
9
- def send_representation(status, content_type, body)
10
- add_header(CONTENT_TYPE, content_type)
11
- send_response(status, body)
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
- # Sends a file representation. If the request method is HEAD, the response
15
- # body is ommitted.
16
- def send_file(status, content_type, fn)
17
- add_header(CONTENT_TYPE, content_type)
18
- if @method == :head
19
- send_response(status, nil, File.size(fn))
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
- send_response(status, IO.read(fn))
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 send_template(status, content_type, template, binding)
26
- body = ServerSide::Template.render(template, binding)
27
- send_representation(status, content_type, body)
78
+ def stream(period, &block)
79
+ @stream_period = period
80
+ @stream_proc = block
81
+ self
28
82
  end
29
83
 
30
- # Sends an error response. The HTTP status code is derived from the error
31
- # class.
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
- # Sends an HTTP response.
37
- def send_response(status, body = nil, content_length = nil)
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, permanent = false)
54
- add_header(LOCATION, location)
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
- # Starts a stream
59
- def start_stream(status, content_type = nil, body = nil)
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 stream(body)
71
- send_data(body)
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
- ROOT_PATH = '/'.freeze
105
+ def self.streaming(opts = nil, &block)
106
+ new(opts).stream(1, &block)
107
+ end
75
108
 
76
- # Adds a cookie to the response headers.
77
- def set_cookie(name, value, expires, path = nil, domain = nil)
78
- path ||= ROOT_PATH
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
- # Adds an expired cookie to the response headers.
87
- def delete_cookie(name, path = nil, domain = nil)
88
- set_cookie(name, nil, COOKIE_EXPIRED_TIME, path, domain)
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