serverside 0.3.1 → 0.4.1

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