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
@@ -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
|