serverside 0.3.1 → 0.4.1

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