serverside 0.3.1 → 0.4.1

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.
@@ -1,26 +0,0 @@
1
- require 'rubygems'
2
- require 'metaid'
3
- require File.join(File.dirname(__FILE__), 'connection')
4
-
5
- module ServerSide
6
- module Application
7
- @@config = nil
8
-
9
- def self.config=(c)
10
- @@config = c
11
- end
12
-
13
- def self.daemonize(config, cmd)
14
- config = @@config.merge(config) if @@config
15
- daemon_class = Class.new(Daemon::Cluster) do
16
- meta_def(:pid_fn) {Daemon::WorkingDirectory/'serverside.pid'}
17
- meta_def(:server_loop) do |port|
18
- ServerSide::HTTP::Server.new(
19
- config[:host], port, ServerSide::Router).start
20
- end
21
- meta_def(:ports) {config[:ports]}
22
- end
23
- Daemon.control(daemon_class, cmd)
24
- end
25
- end
26
- end
@@ -1,91 +0,0 @@
1
- require 'time'
2
-
3
- module ServerSide
4
- module HTTP
5
- # This module implements HTTP cache negotiation with a client.
6
- module Caching
7
- # HTTP headers
8
- ETAG = 'ETag'.freeze
9
- LAST_MODIFIED = 'Last-Modified'.freeze
10
- EXPIRES = 'Expires'.freeze
11
- CACHE_CONTROL = 'Cache-Control'.freeze
12
- VARY = 'Vary'.freeze
13
-
14
- IF_NONE_MATCH = 'If-None-Match'.freeze
15
- IF_MODIFIED_SINCE = 'If-Modified-Since'.freeze
16
- WILDCARD = '*'.freeze
17
-
18
- # Header values
19
- NO_CACHE = 'no-cache'.freeze
20
- IF_NONE_MATCH_REGEXP = /^"?([^"]+)"?$/.freeze
21
-
22
- # etags
23
- EXPIRY_ETAG_REGEXP = /(\d+)-(\d+)/.freeze
24
- EXPIRY_ETAG_FORMAT = "%d-%d".freeze
25
- ETAG_QUOTE_FORMAT = '"%s"'.freeze
26
-
27
- # 304 formats
28
- NOT_MODIFIED_CLOSE = "HTTP/1.1 304 Not Modified\r\nDate: %s\r\nConnection: close\r\nContent-Length: 0\r\n\r\n".freeze
29
- NOT_MODIFIED_PERSIST = "HTTP/1.1 304 Not Modified\r\nDate: %s\r\nContent-Length: 0\r\n\r\n".freeze
30
-
31
- def disable_caching
32
- @response_headers[CACHE_CONTROL] = NO_CACHE
33
- @response_headers.delete(ETAG)
34
- @response_headers.delete(LAST_MODIFIED)
35
- @response_headers.delete(EXPIRES)
36
- @response_headers.delete(VARY)
37
- end
38
-
39
- def etag_validators
40
- h = @headers[IF_NONE_MATCH]
41
- return [] unless h
42
- h.split(',').inject([]) do |m, i|
43
- i.strip =~ IF_NONE_MATCH_REGEXP ? (m << $1) : m
44
- end
45
- end
46
-
47
- def valid_etag?(etag = nil)
48
- if etag
49
- etag_validators.each {|e| return true if e == etag || e == WILDCARD}
50
- else
51
- etag_validators.each do |e|
52
- return true if e == WILDCARD ||
53
- ((e =~ EXPIRY_ETAG_REGEXP) && (Time.at($2.to_i) > Time.now))
54
- end
55
- end
56
- nil
57
- end
58
-
59
- def expiry_etag(stamp, max_age)
60
- EXPIRY_ETAG_FORMAT % [stamp.to_i, (stamp + max_age).to_i]
61
- end
62
-
63
- def valid_stamp?(stamp)
64
- return true if (modified_since = @headers[IF_MODIFIED_SINCE]) &&
65
- (modified_since == stamp.httpdate)
66
- end
67
-
68
- def validate_cache(stamp, max_age, etag = nil,
69
- cache_control = nil, vary = nil, &block)
70
-
71
- if valid_etag?(etag) || valid_stamp?(stamp)
72
- send_not_modified_response
73
- true
74
- else
75
- @response_headers[ETAG] = ETAG_QUOTE_FORMAT %
76
- [etag || expiry_etag(stamp, max_age)]
77
- @response_headers[LAST_MODIFIED] = stamp.httpdate
78
- @response_headers[EXPIRES] = (Time.now + max_age).httpdate
79
- @response_headers[CACHE_CONTROL] = cache_control if cache_control
80
- @response_headers[VARY] = vary if vary
81
- block ? block.call : nil
82
- end
83
- end
84
-
85
- def send_not_modified_response
86
- @socket << ((@persistent ? NOT_MODIFIED_PERSIST : NOT_MODIFIED_CLOSE) %
87
- Time.now.httpdate)
88
- end
89
- end
90
- end
91
- end
@@ -1,34 +0,0 @@
1
- require File.join(File.dirname(__FILE__), 'static')
2
-
3
- module ServerSide
4
- module HTTP
5
- # The Connection class represents HTTP connections. Each connection
6
- # instance creates a separate thread for execution and processes
7
- # incoming requests in a loop until the connection is closed by
8
- # either server or client, thus implementing HTTP 1.1 persistent
9
- # connections.
10
- class Connection
11
- # Initializes the request instance. A new thread is created for
12
- # processing requests.
13
- def initialize(socket, request_class)
14
- @socket, @request_class = socket, request_class
15
- @thread = Thread.new {process}
16
- end
17
-
18
- # Processes incoming requests by parsing them and then responding. If
19
- # any error occurs, or the connection is not persistent, the connection
20
- # is closed.
21
- def process
22
- while true
23
- # the process function is expected to return true or a non-nil value
24
- # if the connection is to persist.
25
- break unless @request_class.new(@socket).process
26
- end
27
- rescue => e
28
- # We don't care. Just close the connection.
29
- ensure
30
- @socket.close
31
- end
32
- end
33
- end
34
- end
@@ -1,91 +0,0 @@
1
- require File.join(File.dirname(__FILE__), 'routing')
2
- require 'rubygems'
3
- require 'metaid'
4
-
5
- module ServerSide
6
- # Implements a basic controller class for handling requests. Controllers can
7
- # be mounted by using the Controller.mount
8
- class Controller
9
- # Creates a subclass of Controller which adds a routing rule when
10
- # subclassed. For example:
11
- #
12
- # class MyController < ServerSide::Controller.mount('/ohmy')
13
- # def response
14
- # render('Hi there!', 'text/plain')
15
- # end
16
- # end
17
- #
18
- # You can of course route according to any rule as specified in
19
- # ServerSide::Router.route, including passing a block as a rule, e.g.:
20
- #
21
- # class MyController < ServerSide::Controller.mount {@headers['Accept'] =~ /wap/}
22
- # ...
23
- # end
24
- def self.mount(rule = nil, &block)
25
- rule ||= block
26
- raise ArgumentError, "No routing rule specified." if rule.nil?
27
- Class.new(self) do
28
- meta_def(:inherited) do |sub_class|
29
- ServerSide::Router.route(rule) {sub_class.new(self)}
30
- end
31
- end
32
- end
33
-
34
- # Initialize a new controller instance. Sets @request to the request object
35
- # and copies both the request path and parameters to instance variables.
36
- # After calling response, this method checks whether a response has been sent
37
- # (rendered), and if not, invokes the render_default method.
38
- def initialize(request)
39
- @request = request
40
- @path = request.path
41
- @parameters = request.parameters
42
- response
43
- render_default if not @rendered
44
- end
45
-
46
- # Renders the response. This method should be overriden.
47
- def response
48
- end
49
-
50
- # Sends a default response.
51
- def render_default
52
- @request.send_response(200, 'text/plain', 'no response.')
53
- end
54
-
55
- # Sends a response and sets @rendered to true.
56
- def render(body, content_type)
57
- @request.send_response(200, content_type, body)
58
- @rendered = true
59
- end
60
- end
61
- end
62
-
63
- __END__
64
-
65
- class ServerSide::ActionController < ServerSide::Controller
66
- def self.default_routing_rule
67
- if name.split('::').last =~ /(.+)Controller$/
68
- controller = Inflector.underscore($1)
69
- {:path => ["/#{controller}", "/#{controller}/:action", "/#{controller}/:action/:id"]}
70
- end
71
- end
72
-
73
- def self.inherited(c)
74
- routing_rule = c.respond_to?(:routing_rule) ?
75
- c.routing_rule : c.default_routing_rule
76
- if routing_rule
77
- ServerSide::Router.route(routing_rule) {c.new(self)}
78
- end
79
- end
80
-
81
- def self.route(arg = nil, &block)
82
- rule = arg || block
83
- meta_def(:get_route) {rule}
84
- end
85
- end
86
-
87
- class MyController < ActionController
88
- route "hello"
89
- end
90
-
91
- p MyController.get_route
@@ -1,210 +0,0 @@
1
- require File.join(File.dirname(__FILE__), 'static')
2
- require 'time'
3
-
4
- module ServerSide
5
- module HTTP
6
- # The Request class encapsulates HTTP requests. The request class
7
- # contains methods for parsing the request and rendering a response.
8
- # HTTP requests are created by the connection. Descendants of HTTPRequest
9
- # can be created
10
- # When a connection is created, it creates new requests in a loop until
11
- # the connection is closed.
12
- class Request
13
-
14
- LINE_BREAK = "\r\n".freeze
15
- # Here's a nice one - parses the first line of a request.
16
- # The expected format is as follows:
17
- # <method> </path>[/][?<query>] HTTP/<version>
18
- REQUEST_REGEXP = /([A-Za-z0-9]+)\s(\/[^\/\?]*(?:\/[^\/\?]+)*)\/?(?:\?(.*))?\sHTTP\/(.+)\r/.freeze
19
- # Regexp for parsing headers.
20
- HEADER_REGEXP = /([^:]+):\s?(.*)\r\n/.freeze
21
- CONTENT_LENGTH = 'Content-Length'.freeze
22
- VERSION_1_1 = '1.1'.freeze
23
- CONNECTION = 'Connection'.freeze
24
- CLOSE = 'close'.freeze
25
- AMPERSAND = '&'.freeze
26
- # Regexp for parsing URI parameters.
27
- PARAMETER_REGEXP = /(.+)=(.*)/.freeze
28
- EQUAL_SIGN = '='.freeze
29
- STATUS_CLOSE = "HTTP/1.1 %d\r\nDate: %s\r\nConnection: close\r\nContent-Type: %s\r\n%s%sContent-Length: %d\r\n\r\n".freeze
30
- STATUS_STREAM = "HTTP/1.1 %d\r\nDate: %s\r\nConnection: close\r\nContent-Type: %s\r\n%s%s\r\n".freeze
31
- STATUS_PERSIST = "HTTP/1.1 %d\r\nDate: %s\r\nContent-Type: %s\r\n%s%sContent-Length: %d\r\n\r\n".freeze
32
- STATUS_REDIRECT = "HTTP/1.1 %d\r\nDate: %s\r\nConnection: close\r\nLocation: %s\r\n\r\n".freeze
33
- HEADER = "%s: %s\r\n".freeze
34
- EMPTY_STRING = ''.freeze
35
- EMPTY_HASH = {}.freeze
36
- SLASH = '/'.freeze
37
- LOCATION = 'Location'.freeze
38
- COOKIE = 'Cookie'
39
- SET_COOKIE = "Set-Cookie: %s=%s; path=/; expires=%s\r\n".freeze
40
- COOKIE_SPLIT = /[;,] */n.freeze
41
- COOKIE_REGEXP = /\s*(.+)=(.*)\s*/.freeze
42
- COOKIE_EXPIRED_TIME = Time.at(0).freeze
43
- CONTENT_TYPE = "Content-Type".freeze
44
- CONTENT_TYPE_URL_ENCODED = 'application/x-www-form-urlencoded'.freeze
45
-
46
- include Static
47
-
48
- attr_reader :socket, :method, :path, :query, :version, :parameters,
49
- :headers, :persistent, :cookies, :response_cookies, :body,
50
- :content_length, :content_type, :response_headers
51
-
52
- # Initializes the request instance. Any descendants of HTTP::Request
53
- # which override the initialize method must receive socket as the
54
- # single argument, and copy it to @socket.
55
- def initialize(socket)
56
- @socket = socket
57
- @response_headers = {}
58
- end
59
-
60
- # Processes the request by parsing it and then responding.
61
- def process
62
- parse && ((respond || true) && @persistent)
63
- end
64
-
65
- # Parses an HTTP request. If the request is not valid, nil is returned.
66
- # Otherwise, the HTTP headers are returned. Also determines whether the
67
- # connection is persistent (by checking the HTTP version and the
68
- # 'Connection' header).
69
- def parse
70
- return nil unless @socket.gets =~ REQUEST_REGEXP
71
- @method, @path, @query, @version = $1.downcase.to_sym, $2, $3, $4
72
- @parameters = @query ? parse_parameters(@query) : {}
73
- @headers = {}
74
- while (line = @socket.gets)
75
- break if line.nil? || (line == LINE_BREAK)
76
- if line =~ HEADER_REGEXP
77
- @headers[$1.freeze] = $2.freeze
78
- end
79
- end
80
- @persistent = (@version == VERSION_1_1) &&
81
- (@headers[CONNECTION] != CLOSE)
82
- @cookies = @headers[COOKIE] ? parse_cookies : EMPTY_HASH
83
- @response_cookies = nil
84
-
85
- if @content_length = @headers[CONTENT_LENGTH].to_i
86
- @content_type = @headers[CONTENT_TYPE] || CONTENT_TYPE_URL_ENCODED
87
- @body = @socket.read(@content_length) rescue nil
88
- parse_body
89
- end
90
-
91
- @headers
92
- end
93
-
94
- # Parses query parameters by splitting the query string and unescaping
95
- # parameter values.
96
- def parse_parameters(query)
97
- query.split(AMPERSAND).inject({}) do |m, i|
98
- if i =~ PARAMETER_REGEXP
99
- m[$1.to_sym] = $2.uri_unescape
100
- end
101
- m
102
- end
103
- end
104
-
105
- # Parses cookie values passed in the request
106
- def parse_cookies
107
- @headers[COOKIE].split(COOKIE_SPLIT).inject({}) do |m, i|
108
- if i =~ COOKIE_REGEXP
109
- m[$1.to_sym] = $2.uri_unescape
110
- end
111
- m
112
- end
113
- end
114
-
115
- MULTIPART_REGEXP = /multipart\/form-data.*boundary=\"?([^\";,]+)/n.freeze
116
- CONTENT_DISPOSITION_REGEXP = /^Content-Disposition: form-data;([^\r]*)/m.freeze
117
- FIELD_ATTRIBUTE_REGEXP = /\s*(\w+)=\"([^\"]*)/.freeze
118
- CONTENT_TYPE_REGEXP = /^Content-Type: ([^\r]*)/m.freeze
119
-
120
- # parses the body, either by using
121
- def parse_body
122
- if @content_type == CONTENT_TYPE_URL_ENCODED
123
- @parameters.merge! parse_parameters(@body)
124
- elsif @content_type =~ MULTIPART_REGEXP
125
- boundary = "--#$1"
126
- r = /(?:\r?\n|\A)#{Regexp::quote("--#$1")}(?:--)?\r\n/m
127
- @body.split(r).each do |pt|
128
- headers, payload = pt.split("\r\n\r\n", 2)
129
- atts = {}
130
- if headers =~ CONTENT_DISPOSITION_REGEXP
131
- $1.split(';').map do |part|
132
- if part =~ FIELD_ATTRIBUTE_REGEXP
133
- atts[$1.to_sym] = $2
134
- end
135
- end
136
- end
137
- if headers =~ CONTENT_TYPE_REGEXP
138
- atts[:type] = $1
139
- end
140
- if name = atts[:name]
141
- atts[:content] = payload
142
- @parameters[name.to_sym] = atts[:filename] ? atts : atts[:content]
143
- end
144
- end
145
- end
146
- end
147
-
148
- # Sends an HTTP response.
149
- def send_response(status, content_type, body = nil, content_length = nil,
150
- headers = nil)
151
- @response_headers.merge!(headers) if headers
152
- h = @response_headers.inject('') {|m, kv| m << (HEADER % kv)}
153
-
154
- # calculate content_length if needed. if we dont have the
155
- # content_length, we consider the response as a streaming response,
156
- # and so the connection will not be persistent.
157
- content_length = body.length if content_length.nil? && body
158
- @persistent = false if content_length.nil?
159
-
160
- # Select the right format to use according to circumstances.
161
- @socket << ((@persistent ? STATUS_PERSIST :
162
- (body ? STATUS_CLOSE : STATUS_STREAM)) %
163
- [status, Time.now.httpdate, content_type, h, @response_cookies,
164
- content_length])
165
- @socket << body if body
166
- rescue
167
- @persistent = false
168
- end
169
-
170
- CONTENT_DISPOSITION = 'Content-Disposition'.freeze
171
- CONTENT_DESCRIPTION = 'Content-Description'.freeze
172
-
173
- def send_file(content, content_type, disposition = :inline,
174
- filename = nil, description = nil)
175
- disposition = filename ?
176
- "#{disposition}; filename=#{filename}" : disposition
177
- @response_headers[CONTENT_DISPOSITION] = disposition
178
- @response_headers[CONTENT_DESCRIPTION] = description if description
179
- send_response(200, content_type, content)
180
- end
181
-
182
- # Send a redirect response.
183
- def redirect(location, permanent = false)
184
- @socket << (STATUS_REDIRECT %
185
- [permanent ? 301 : 302, Time.now.httpdate, location])
186
- rescue
187
- ensure
188
- @persistent = false
189
- end
190
-
191
- # Streams additional data to the client.
192
- def stream(body)
193
- (@socket << body if body) rescue (@persistent = false)
194
- end
195
-
196
- # Sets a cookie to be included in the response.
197
- def set_cookie(name, value, expires)
198
- @response_cookies ||= ""
199
- @response_cookies <<
200
- (SET_COOKIE % [name, value.to_s.uri_escape, expires.rfc2822])
201
- end
202
-
203
- # Marks a cookie as deleted. The cookie is given an expires stamp in
204
- # the past.
205
- def delete_cookie(name)
206
- set_cookie(name, nil, COOKIE_EXPIRED_TIME)
207
- end
208
- end
209
- end
210
- end
@@ -1,133 +0,0 @@
1
- require File.join(File.dirname(__FILE__), 'request')
2
-
3
- module ServerSide
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
- # lambdas or by hashes that contain variable names corresponding to patterns.
7
- #
8
- # The simplest form of a routing rule specifies a path pattern:
9
- #
10
- # ServerSide.route('/static') {serve_static('.'/@path)}
11
- #
12
- # But you can also check for other attributes of the request:
13
- #
14
- # ServerSide.route(:path => '/static', :host => '^:subdomain\.mydomain') {
15
- # serve_static(@parameters[:subdomain]/@path)
16
- # }
17
- #
18
- # It also possible to pass a lambda as a rule:
19
- #
20
- # ServerSide.route(lambda {@headers['Agent'] =~ /Moz/}) {serve_static('moz'/@path)}
21
- #
22
- # Routing rules are evaluated backwards, so the rules should be ordered
23
- # from the general to the specific.
24
- class Router < HTTP::Request
25
- @@rules = []
26
- @@default_route = nil
27
-
28
- # Returns true if routes were defined.
29
- def self.routes_defined?
30
- !@@rules.empty? || @@default_route
31
- end
32
-
33
- # Adds a routing rule. The normalized rule is a hash containing keys (acting
34
- # as instance variable names) with patterns as values. If the rule is not a
35
- # hash, it is normalized into a pattern checked against the request path.
36
- # Pattern values can also be arrays, any member of which is checked as a
37
- # pattern. The rule can also be a Proc or lambda which is run with the
38
- # connection object's binding. A contrived example:
39
- #
40
- # ServerSide.route(lambda{path = 'mypage'}) {serve_static('mypage.html')}
41
- def self.route(rule, &block)
42
- rule = {:path => rule} unless (Hash === rule) || (Proc === rule)
43
- @@rules.unshift [rule, block]
44
- compile_rules
45
- end
46
-
47
- # Compiles all rules into a respond method that is invoked when a request
48
- # is received.
49
- def self.compile_rules
50
- code = @@rules.inject('lambda {') {|m, r| m << rule_to_statement(r[0], r[1])}
51
- code << 'default_handler}'
52
- define_method(:respond, &eval(code))
53
- end
54
-
55
- # Converts a rule into an if statement. All keys in the rule are matched
56
- # against their respective values.
57
- def self.rule_to_statement(rule, block)
58
- proc_tag = define_proc(&block)
59
- if Proc === rule
60
- cond = define_proc(&rule).to_s
61
- else
62
- cond = rule.to_a.map {|kv|
63
- if Array === kv[1]
64
- '(' + kv[1].map {|v| condition_part(kv[0], v)}.join('||') + ')'
65
- else
66
- condition_part(kv[0], kv[1])
67
- end
68
- }.join('&&')
69
- end
70
- "if #{cond} && (r = #{proc_tag}); return r; end\n"
71
- end
72
-
73
- # Pattern for finding parameters inside patterns. Parameters are parts
74
- # of the pattern, which the routing pre-processor turns into sub-regexp
75
- # that are used to extract parameter values from the pattern.
76
- #
77
- # For example, matching '/controller/show' against '/controller/:action'
78
- # will give us @parameters[:action] #=> "show"
79
- ParamRegexp = /(?::([a-z]+))/
80
-
81
- # Returns the condition part for the key and value specified. The key is the
82
- # name of an instance variable and the value is a pattern to match against.
83
- # If the pattern contains parameters (for example, /controller/:action,) the
84
- # method creates a lambda for extracting the parameter values.
85
- def self.condition_part(key, value)
86
- p_parse, p_count = '', 0
87
- while (String === value) && (value =~ ParamRegexp)
88
- value = value.dup
89
- p_name = $1
90
- p_count += 1
91
- value.sub!(ParamRegexp, '(.+)')
92
- p_parse << "@parameters[:#{p_name}] = $#{p_count}\n"
93
- end
94
- cond = "(@#{key} =~ #{cache_constant(Regexp.new(value))})"
95
- if p_count == 0
96
- cond
97
- else
98
- tag = define_proc(&eval(
99
- "lambda {if #{cond}\n#{p_parse}true\nelse\nfalse\nend}"))
100
- "(#{tag})"
101
- end
102
- end
103
-
104
- # Converts a proc into a method, returning the method's name (as a symbol)
105
- def self.define_proc(&block)
106
- tag = block.proc_tag
107
- define_method(tag.to_sym, &block) unless instance_methods.include?(tag)
108
- tag.to_sym
109
- end
110
-
111
- # Converts a value into a local constant and freezes it. Returns the
112
- # constant's tag name
113
- def self.cache_constant(value)
114
- tag = value.const_tag
115
- class_eval "#{tag} = #{value.inspect}.freeze" rescue nil
116
- tag
117
- end
118
-
119
- # Sets the default handler for incoming requests.
120
- def self.default_route(&block)
121
- @@default_route = block
122
- define_method(:default_handler, &block)
123
- compile_rules
124
- end
125
-
126
- # Generic responder for unhandled requests.
127
- def unhandled
128
- send_response(403, 'text', 'No handler found.')
129
- end
130
-
131
- alias_method :default_handler, :unhandled
132
- end
133
- end
@@ -1,27 +0,0 @@
1
- require 'socket'
2
-
3
- module ServerSide
4
- module HTTP
5
- # The ServerSide HTTP server is designed to be fast and simple. It is also
6
- # designed to support both HTTP 1.1 persistent connections, and HTTP streaming
7
- # for applications which use Comet techniques.
8
- class Server
9
- attr_reader :listener
10
-
11
- # Creates a new server by opening a listening socket.
12
- def initialize(host, port, request_class)
13
- @request_class = request_class
14
- @listener = TCPServer.new(host, port)
15
- end
16
-
17
- # starts an accept loop. When a new connection is accepted, a new
18
- # instance of the supplied connection class is instantiated and passed
19
- # the connection for processing.
20
- def start
21
- while true
22
- Connection.new(@listener.accept, @request_class)
23
- end
24
- end
25
- end
26
- end
27
- end
@@ -1,82 +0,0 @@
1
- require File.join(File.dirname(__FILE__), 'caching')
2
-
3
- module ServerSide
4
- # This module provides functionality for serving files and directory listings
5
- # over HTTP.
6
- module Static
7
- include HTTP::Caching
8
-
9
- ETAG_FORMAT = '%x:%x:%x'.freeze
10
- TEXT_PLAIN = 'text/plain'.freeze
11
- TEXT_HTML = 'text/html'.freeze
12
- MAX_CACHE_FILE_SIZE = 100000.freeze # 100KB for the moment
13
- MAX_AGE = 86400 # one day
14
-
15
- DIR_LISTING_START = '<html><head><title>Directory Listing for %s</title></head><body><h2>Directory listing for %s:</h2>'.freeze
16
- DIR_LISTING = '<a href="%s">%s</a><br/>'.freeze
17
- DIR_LISTING_STOP = '</body></html>'.freeze
18
- FILE_NOT_FOUND = 'File not found.'.freeze
19
- RHTML = /\.rhtml$/.freeze
20
-
21
- @@mime_types = Hash.new {|h, k| TEXT_PLAIN}
22
- @@mime_types.merge!({
23
- '.html'.freeze => 'text/html'.freeze,
24
- '.css'.freeze => 'text/css'.freeze,
25
- '.js'.freeze => 'text/javascript'.freeze,
26
-
27
- '.gif'.freeze => 'image/gif'.freeze,
28
- '.jpg'.freeze => 'image/jpeg'.freeze,
29
- '.jpeg'.freeze => 'image/jpeg'.freeze,
30
- '.png'.freeze => 'image/png'.freeze
31
- })
32
-
33
- # Serves a file over HTTP. The file is cached in memory for later retrieval.
34
- # If the If-None-Match header is included with an ETag, it is checked
35
- # against the file's current ETag. If there's a match, a 304 response is
36
- # rendered.
37
- def serve_file(fn)
38
- stat = File.stat(fn)
39
- etag = (ETAG_FORMAT % [stat.mtime.to_i, stat.size, stat.ino]).freeze
40
- validate_cache(stat.mtime, MAX_AGE, etag) do
41
- send_response(200, @@mime_types[File.extname(fn)], IO.read(fn),
42
- stat.size)
43
- end
44
- rescue => e
45
- send_response(404, TEXT_PLAIN, 'Error reading file.')
46
- end
47
-
48
- # Serves a directory listing over HTTP in the form of an HTML page.
49
- def serve_dir(dir)
50
- entries = Dir.entries(dir)
51
- entries.reject! {|fn| fn =~ /^\./}
52
- entries.unshift('..') if dir != './'
53
- html = (DIR_LISTING_START % [@path, @path]) +
54
- entries.inject('') {|m, fn| m << DIR_LISTING % [@path/fn, fn]} +
55
- DIR_LISTING_STOP
56
- send_response(200, 'text/html', html)
57
- end
58
-
59
- def serve_template(fn, b = nil)
60
- send_response(200, TEXT_HTML, Template.render(fn, b || binding))
61
- end
62
-
63
- # Serves static files and directory listings.
64
- def serve_static(path)
65
- if File.file?(path)
66
- path =~ RHTML ? serve_template(path) : serve_file(path)
67
- elsif File.directory?(path)
68
- if File.file?(path/'index.html')
69
- serve_file(path/'index.html')
70
- elsif File.file?(path/'index.rhtml')
71
- serve_template(path/'index.rhtml')
72
- else
73
- serve_dir(path)
74
- end
75
- else
76
- send_response(404, 'text', FILE_NOT_FOUND)
77
- end
78
- rescue => e
79
- send_response(500, 'text', e.message)
80
- end
81
- end
82
- end