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