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 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