serverside 0.3.1 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- 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'
|