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.
- data/README +15 -11
- data/Rakefile +18 -18
- data/bin/serverside +20 -16
- data/lib/serverside/cluster.rb +4 -33
- data/lib/serverside/core_ext.rb +56 -7
- data/lib/serverside/daemon.rb +10 -17
- data/lib/serverside/http/caching.rb +79 -0
- data/lib/serverside/http/const.rb +69 -0
- data/lib/serverside/http/error.rb +24 -0
- data/lib/serverside/http/parsing.rb +175 -0
- data/lib/serverside/http/response.rb +91 -0
- data/lib/serverside/http/server.rb +194 -0
- data/lib/serverside/http/static.rb +72 -0
- data/lib/serverside/http.rb +14 -0
- data/lib/serverside/js.rb +173 -0
- data/lib/serverside/log.rb +79 -0
- data/lib/serverside/template.rb +5 -4
- data/lib/serverside/xml.rb +84 -0
- data/lib/serverside.rb +11 -2
- data/spec/core_ext_spec.rb +13 -58
- data/spec/daemon_spec.rb +61 -28
- data/spec/http_spec.rb +259 -0
- data/spec/template_spec.rb +9 -7
- metadata +42 -28
- data/CHANGELOG +0 -261
- data/lib/serverside/application.rb +0 -26
- data/lib/serverside/caching.rb +0 -91
- data/lib/serverside/connection.rb +0 -34
- data/lib/serverside/controllers.rb +0 -91
- data/lib/serverside/request.rb +0 -210
- data/lib/serverside/routing.rb +0 -133
- data/lib/serverside/server.rb +0 -27
- data/lib/serverside/static.rb +0 -82
- data/spec/caching_spec.rb +0 -318
- data/spec/cluster_spec.rb +0 -140
- data/spec/connection_spec.rb +0 -59
- data/spec/controllers_spec.rb +0 -142
- data/spec/request_spec.rb +0 -288
- data/spec/routing_spec.rb +0 -240
- data/spec/server_spec.rb +0 -40
- data/spec/static_spec.rb +0 -279
@@ -0,0 +1,175 @@
|
|
1
|
+
module ServerSide::HTTP
|
2
|
+
module Parsing
|
3
|
+
REQUEST_LINE_RE = /([A-Za-z0-9]+)\s(\/[^\/\?]*(?:\/[^\/\?]+)*)\/?(?:\?(.*))?\sHTTP\/(.+)/.freeze
|
4
|
+
|
5
|
+
# Parses a request line into a method, URI, query and HTTP version parts.
|
6
|
+
# If a query is included, it is parsed into query parameters.
|
7
|
+
def parse_request_line(line)
|
8
|
+
if line =~ REQUEST_LINE_RE
|
9
|
+
@request_line = line
|
10
|
+
@method, @uri, @query, @http_version = $1.downcase.to_sym, $2, $3, $4
|
11
|
+
@params = @query ? parse_query_parameters(@query) : {}
|
12
|
+
else
|
13
|
+
raise MalformedRequestError, "Invalid request format"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
HEADER_RE = /([^:]+):\s*(.*)/.freeze
|
18
|
+
|
19
|
+
# Parses an HTTP header.
|
20
|
+
def parse_header(line)
|
21
|
+
if line =~ HEADER_RE
|
22
|
+
k = $1.freeze
|
23
|
+
v = $2.freeze
|
24
|
+
case k
|
25
|
+
when CONTENT_LENGTH: @content_length = v.to_i
|
26
|
+
when CONNECTION: @persistent = v == KEEP_ALIVE
|
27
|
+
when COOKIE: parse_cookies(v)
|
28
|
+
end
|
29
|
+
@request_headers[k] = v
|
30
|
+
else
|
31
|
+
raise MalformedRequestError, "Invalid header format"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
AMPERSAND = '&'.freeze
|
36
|
+
PARAMETER_RE = /^(.{1,64})=(.{0,8192})$/.freeze
|
37
|
+
|
38
|
+
# Parses query parameters by splitting the query string and unescaping
|
39
|
+
# parameter values.
|
40
|
+
def parse_query_parameters(query)
|
41
|
+
query.split(AMPERSAND).inject({}) do |m, i|
|
42
|
+
if i =~ PARAMETER_RE
|
43
|
+
m[$1.to_sym] = $2.uri_unescape
|
44
|
+
else
|
45
|
+
raise MalformedRequestError, "Invalid parameter format"
|
46
|
+
end
|
47
|
+
m
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
COOKIE_RE = /^(.+)=(.*)$/.freeze
|
52
|
+
SEMICOLON = ';'.freeze
|
53
|
+
|
54
|
+
# Parses a cookies header.
|
55
|
+
def parse_cookies(cookies)
|
56
|
+
cookies.split(SEMICOLON).each do |c|
|
57
|
+
if c.strip =~ COOKIE_RE
|
58
|
+
@request_cookies[$1.to_sym] = $2.uri_unescape
|
59
|
+
else
|
60
|
+
raise MalformedRequestError, "Invalid cookie format"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
BOUNDARY_FIX = '--'.freeze
|
66
|
+
|
67
|
+
# Parses the request body.
|
68
|
+
def parse_request_body(body)
|
69
|
+
case @request_headers[CONTENT_TYPE]
|
70
|
+
when MULTIPART_FORM_DATA_RE:
|
71
|
+
parse_multi_part(body, BOUNDARY_FIX + $1) # body.dup so we keep the original request body?
|
72
|
+
when FORM_URL_ENCODED:
|
73
|
+
parse_form_url_encoded(body)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Parses a multipart request body.
|
78
|
+
def parse_multi_part(body, boundary)
|
79
|
+
while part = body.get_up_to_boundary_with_crlf(boundary)
|
80
|
+
unless part.empty?
|
81
|
+
parse_part(part)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Parses a part of a multipart body.
|
87
|
+
def parse_part(part)
|
88
|
+
part_name = nil
|
89
|
+
file_name = nil
|
90
|
+
file_type = nil
|
91
|
+
# part headers
|
92
|
+
while (line = part.get_line)
|
93
|
+
break if line.empty?
|
94
|
+
if line =~ HEADER_RE
|
95
|
+
k = $1.freeze
|
96
|
+
v = $2.freeze
|
97
|
+
case k
|
98
|
+
when CONTENT_DISPOSITION:
|
99
|
+
case v
|
100
|
+
when DISPOSITION_FORM_DATA_RE:
|
101
|
+
p [$1, $2, $3]
|
102
|
+
part_name = $1.to_sym
|
103
|
+
file_name = $3
|
104
|
+
end
|
105
|
+
when CONTENT_TYPE:
|
106
|
+
file_type = v
|
107
|
+
end
|
108
|
+
else
|
109
|
+
raise MalformedRequestError, "Invalid header in part"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
# check if we got the content name
|
113
|
+
unless part_name
|
114
|
+
raise MalformedRequestError, "Invalid part content"
|
115
|
+
end
|
116
|
+
# part body
|
117
|
+
part_body = part.chomp! # what's left of it
|
118
|
+
@params ||= {}
|
119
|
+
@params[part_name] = file_name ?
|
120
|
+
{:file_name => file_name, :file_content => part_body, :file_type => file_type} :
|
121
|
+
part_body
|
122
|
+
end
|
123
|
+
|
124
|
+
# Parses query parameters passed in the body (for POST requests.)
|
125
|
+
def parse_form_url_encoded(body)
|
126
|
+
@params ||= {}
|
127
|
+
@params.merge!(parse_query_parameters(body))
|
128
|
+
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
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module ServerSide::HTTP
|
2
|
+
module Response
|
3
|
+
# Adds a header to the response.
|
4
|
+
def add_header(k, v)
|
5
|
+
@response_headers << "#{k}: #{v}\r\n"
|
6
|
+
end
|
7
|
+
|
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)
|
12
|
+
end
|
13
|
+
|
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))
|
20
|
+
else
|
21
|
+
send_response(status, IO.read(fn))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def send_template(status, content_type, template, binding)
|
26
|
+
body = ServerSide::Template.render(template, binding)
|
27
|
+
send_representation(status, content_type, body)
|
28
|
+
end
|
29
|
+
|
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)
|
34
|
+
end
|
35
|
+
|
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}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def redirect(location, permanent = false)
|
54
|
+
add_header(LOCATION, location)
|
55
|
+
send_response(permanent ? STATUS_MOVED_PERMANENTLY : STATUS_FOUND,'')
|
56
|
+
end
|
57
|
+
|
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
|
68
|
+
end
|
69
|
+
|
70
|
+
def stream(body)
|
71
|
+
send_data(body)
|
72
|
+
end
|
73
|
+
|
74
|
+
ROOT_PATH = '/'.freeze
|
75
|
+
|
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)
|
84
|
+
end
|
85
|
+
|
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)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,194 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'eventmachine'
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
# Use epoll if available
|
6
|
+
EventMachine.epoll
|
7
|
+
|
8
|
+
module ServerSide::HTTP
|
9
|
+
# The HTTP server is implemented as a simple state-machine with the following
|
10
|
+
# states:
|
11
|
+
# state_initial - initialize request variables.
|
12
|
+
# state_request_line - wait for and parse the request line.
|
13
|
+
# state_request_headers - wait for and parse header lines.
|
14
|
+
# state_request_body - wait for and parse the request body.
|
15
|
+
# state_response - send a response.
|
16
|
+
# state_done - the connection is closed.
|
17
|
+
#
|
18
|
+
# The server supports persistent connections (if the request is in HTTP 1.1).
|
19
|
+
# In that case, after responding to the request the state is changed back to
|
20
|
+
# request_line.
|
21
|
+
module Server
|
22
|
+
# Creates a new server module
|
23
|
+
def self.new
|
24
|
+
Module.new do
|
25
|
+
# include the HTTP state machine and everything else
|
26
|
+
include ServerSide::HTTP::Server
|
27
|
+
|
28
|
+
# define a start method for starting the server
|
29
|
+
def self.start(addr, port)
|
30
|
+
EventMachine::run do
|
31
|
+
EventMachine::start_server addr, port, self
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# invoke the supplied block for application-specific behaviors.
|
36
|
+
yield
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# include the Parsing, Response and Caching modules.
|
41
|
+
include ServerSide::HTTP::Parsing
|
42
|
+
include ServerSide::HTTP::Response
|
43
|
+
include ServerSide::HTTP::Caching
|
44
|
+
|
45
|
+
# attribute readers
|
46
|
+
attr_reader :request_line, :method, :uri, :query, :http_version, :params
|
47
|
+
attr_reader :content_length, :persistent, :request_headers
|
48
|
+
attr_reader :request_cookies, :request_body
|
49
|
+
|
50
|
+
# post_init creates a new @in buffer and sets the connection state to
|
51
|
+
# initial.
|
52
|
+
def post_init
|
53
|
+
# initialize the in buffer
|
54
|
+
@in = ''
|
55
|
+
|
56
|
+
# set state to initial
|
57
|
+
set_state(:state_initial)
|
58
|
+
end
|
59
|
+
|
60
|
+
# receive_data is a callback invoked whenever new data is received on the
|
61
|
+
# connection. The incoming data is added to the @in buffer and the state
|
62
|
+
# method is invoked.
|
63
|
+
def receive_data(data)
|
64
|
+
@in << data
|
65
|
+
send(@state)
|
66
|
+
rescue => e
|
67
|
+
# if an error is raised, we send an error response
|
68
|
+
send_error_response(e) unless @state == :done
|
69
|
+
end
|
70
|
+
|
71
|
+
# set_state is called whenever a state transition occurs. It invokes the
|
72
|
+
# state method.
|
73
|
+
def set_state(s)
|
74
|
+
@state = s
|
75
|
+
send(s)
|
76
|
+
rescue => e
|
77
|
+
# if an error is raised, we send an error response
|
78
|
+
send_error_response(e) unless @state == :done
|
79
|
+
end
|
80
|
+
|
81
|
+
# state_initial initializes @request_headers, @request_header_count,
|
82
|
+
# @request_cookies and @response_headers. It immediately transitions to the
|
83
|
+
# request_line state.
|
84
|
+
def state_initial
|
85
|
+
# initialize request and response variables
|
86
|
+
@request_line = nil
|
87
|
+
@response_sent = false
|
88
|
+
@request_headers = {}
|
89
|
+
@request_header_count = 0
|
90
|
+
@request_cookies = {}
|
91
|
+
@response_headers = []
|
92
|
+
@content_length = nil
|
93
|
+
|
94
|
+
# immediately transition to the request_line state
|
95
|
+
set_state(:state_request_line)
|
96
|
+
end
|
97
|
+
|
98
|
+
# state_request_line waits for the HTTP request line and parses it once it
|
99
|
+
# arrives. If the request line is too big, an error is raised. The request
|
100
|
+
# line supplies information including the
|
101
|
+
def state_request_line
|
102
|
+
# check request line size
|
103
|
+
if @in.size > MAX_REQUEST_LINE_SIZE
|
104
|
+
raise MalformedRequestError, "Invalid request size"
|
105
|
+
end
|
106
|
+
if line = @in.get_line
|
107
|
+
parse_request_line(line)
|
108
|
+
# HTTP 1.1 connections are persistent by default.
|
109
|
+
@persistent = @http_version == VERSION_1_1
|
110
|
+
set_state(:state_request_headers)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# state_request_headers parses each header as it arrives. If too many
|
115
|
+
# headers are included or a header exceeds the maximum header size,
|
116
|
+
# an error is raised.
|
117
|
+
def state_request_headers
|
118
|
+
while line = @in.get_line
|
119
|
+
# Check header size
|
120
|
+
if line.size > MAX_HEADER_SIZE
|
121
|
+
raise MalformedRequestError, "Invalid header size"
|
122
|
+
# If the line empty then we move to the next state
|
123
|
+
elsif line.empty?
|
124
|
+
expecting_body = @content_length && (@content_length > 0)
|
125
|
+
set_state(expecting_body ? :state_request_body : :state_response)
|
126
|
+
else
|
127
|
+
# check header count
|
128
|
+
if (@request_header_count += 1) > MAX_HEADER_COUNT
|
129
|
+
raise MalformedRequestError, "Too many headers"
|
130
|
+
else
|
131
|
+
parse_header(line)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# state_request_body waits for the request body to arrive and then parses
|
138
|
+
# the body. Once the body is parsed, the connection transitions to the
|
139
|
+
# response state.
|
140
|
+
def state_request_body
|
141
|
+
if @in.size >= @content_length
|
142
|
+
@request_body = @in.slice!(0...@content_length)
|
143
|
+
parse_request_body(@request_body)
|
144
|
+
set_state(:state_response)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# state_response invokes the handle method. If no response was sent, an
|
149
|
+
# error is raised. After the response is sent, the connection is either
|
150
|
+
# closed or goes back to the initial state.
|
151
|
+
def state_response
|
152
|
+
handle
|
153
|
+
unless @response_sent || @streaming
|
154
|
+
raise "No handler found for this URI (#{@uri})"
|
155
|
+
end
|
156
|
+
ensure
|
157
|
+
unless @streaming
|
158
|
+
set_state(@persistent ? :state_initial : :state_done)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# state_done closes the connection.
|
163
|
+
def state_done
|
164
|
+
close_connection_after_writing
|
165
|
+
end
|
166
|
+
|
167
|
+
# periodically implements a periodical timer. The timer is invoked until
|
168
|
+
# the supplied block returns false or nil.
|
169
|
+
def periodically(period, &block)
|
170
|
+
EventMachine::add_timer(period) do
|
171
|
+
if block.call
|
172
|
+
periodically(period, &block)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# periodically implements a periodical timer. The timer is invoked until
|
178
|
+
# the supplied block returns false or nil.
|
179
|
+
def streaming_periodically(period, &block)
|
180
|
+
@streaming = true
|
181
|
+
if block.call # invoke block for the first time
|
182
|
+
EventMachine::add_timer(period) do
|
183
|
+
if block.call
|
184
|
+
streaming_periodically(period, &block)
|
185
|
+
else
|
186
|
+
set_state(@persistent ? :state_initial : :state_done)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
else
|
190
|
+
set_state(@persistent ? :state_initial : :state_done)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module ServerSide::HTTP
|
2
|
+
module Static
|
3
|
+
MIME_TYPES = {
|
4
|
+
:html => 'text/html'.freeze,
|
5
|
+
:css => 'text/css'.freeze,
|
6
|
+
:js => 'text/javascript'.freeze,
|
7
|
+
|
8
|
+
:gif => 'image/gif'.freeze,
|
9
|
+
:jpg => 'image/jpeg'.freeze,
|
10
|
+
:jpeg => 'image/jpeg'.freeze,
|
11
|
+
:png => 'image/png'.freeze,
|
12
|
+
:ico => 'image/x-icon'.freeze
|
13
|
+
}
|
14
|
+
MIME_TYPES.default = 'text/plain'.freeze
|
15
|
+
|
16
|
+
CACHE_AGES = {}
|
17
|
+
CACHE_AGES.default = 86400 # one day
|
18
|
+
|
19
|
+
INVALID_PATH_RE = /\.\./.freeze
|
20
|
+
|
21
|
+
# Serves a static file or directory.
|
22
|
+
def serve_static(fn)
|
23
|
+
if fn =~ INVALID_PATH_RE
|
24
|
+
raise MalformedRequestError, "Invalid path specified (#{@uri})"
|
25
|
+
elsif !File.exists?(fn)
|
26
|
+
raise FileNotFoundError, "File not found (#{@uri})"
|
27
|
+
end
|
28
|
+
|
29
|
+
if File.directory?(fn)
|
30
|
+
send_directory_representation(fn)
|
31
|
+
else
|
32
|
+
send_file_representation(fn)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Sends a file representation, setting caching-related headers.
|
37
|
+
def send_file_representation(fn)
|
38
|
+
ext = File.extension(fn)
|
39
|
+
expires = Time.now + CACHE_AGES[ext]
|
40
|
+
cache :etag => File.etag(fn), :expires => expires, :last_modified => File.mtime(fn) do
|
41
|
+
send_file(STATUS_OK, MIME_TYPES[ext], fn)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
DIR_TEMPLATE = '<html><head><title>Directory Listing for %s</title></head><body><h2>Directory listing for %s:</h2><ul>%s</ul></body></html>'.freeze
|
46
|
+
DIR_LISTING = '<li><a href="%s">%s</a><br/></li>'.freeze
|
47
|
+
|
48
|
+
# Sends a directory representation.
|
49
|
+
def send_directory_representation(dir)
|
50
|
+
entries = Dir.entries(dir)
|
51
|
+
entries.reject! {|fn| fn =~ /^\./}
|
52
|
+
entries.unshift('..') if dir != './'
|
53
|
+
|
54
|
+
list = entries.map {|e| DIR_LISTING % [@uri/e, e]}.join
|
55
|
+
html = DIR_TEMPLATE % [dir, dir, list]
|
56
|
+
send_representation(STATUS_OK, MIME_TYPES[:html], html)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class File
|
62
|
+
# Returns an Apache-style ETag for the specified file.
|
63
|
+
def self.etag(fn)
|
64
|
+
stat = File.stat(fn)
|
65
|
+
"#{stat.mtime.to_i.to_s(16)}-#{stat.size.to_s(16)}-#{stat.ino.to_s(16)}"
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns the extension for the file name as a symbol.
|
69
|
+
def self.extension(fn)
|
70
|
+
(ext = (fn =~ /\.([^\.\/]+)$/) && $1) ? ext.to_sym : nil
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module ServerSide
|
2
|
+
module HTTP
|
3
|
+
end
|
4
|
+
end
|
5
|
+
|
6
|
+
http_dir = File.dirname(__FILE__)/'http'
|
7
|
+
|
8
|
+
require http_dir/'const'
|
9
|
+
require http_dir/'error'
|
10
|
+
require http_dir/'parsing'
|
11
|
+
require http_dir/'response'
|
12
|
+
require http_dir/'caching'
|
13
|
+
require http_dir/'static'
|
14
|
+
require http_dir/'server'
|