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
@@ -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
|
data/lib/serverside/caching.rb
DELETED
@@ -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
|
data/lib/serverside/request.rb
DELETED
@@ -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
|
data/lib/serverside/routing.rb
DELETED
@@ -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
|
data/lib/serverside/server.rb
DELETED
@@ -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
|
data/lib/serverside/static.rb
DELETED
@@ -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
|