Class: Anorexic::HTTPProtocol

Inherits:
Object
  • Object
show all
Defined in:
lib/anorexic/server/protocols/http_protocol.rb

Overview

this module is the protocol (controller) for the HTTP server.

to do: implemet logging, support body types: multipart (non-ASCII form data / uploaded files), json & xml

Constant Summary collapse

HTTP_METHODS =
%w{GET HEAD POST PUT DELETE TRACE OPTIONS}

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(service, params) ⇒ HTTPProtocol

Returns a new instance of HTTPProtocol.


13
14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/anorexic/server/protocols/http_protocol.rb', line 13

def initialize service, params
	@service = service
	@parser_stage = 0
	@parser_data = {}
	@parser_body = ''
	@parser_chunk = ''
	@parser_length = 0
	@locker = Mutex.new
	@@rack_dictionary ||= {"HOST".freeze => :host_name, 'REQUEST_METHOD'.freeze => :method,
						'PATH_INFO'.freeze => :path, 'QUERY_STRING'.freeze => :query,
						'SERVER_NAME'.freeze => :host_name, 'SERVER_PORT'.freeze => :port,
						'rack.url_scheme'.freeze => :requested_protocol}
end

Instance Attribute Details

#serviceObject

Returns the value of attribute service


11
12
13
# File 'lib/anorexic/server/protocols/http_protocol.rb', line 11

def service
  @service
end

Instance Method Details

#complete_requestObject

completes the parsing of the request and sends the request to the handler.


166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/anorexic/server/protocols/http_protocol.rb', line 166

def complete_request
	#finalize params and query properties
	m = @parser_data[:query].match /(([a-z0-9A-Z]+):\/\/)?(([^\/\:]+))?(:([0-9]+))?([^\?\#]*)(\?([^\#]*))?/
	@parser_data[:requested_protocol] = m[1] || (service.ssl? ? 'https' : 'http')
	@parser_data[:host_name] = m[4] || (@parser_data['host'] ? @parser_data['host'].match(/^[^:]*/).to_s : nil)
	@parser_data[:port] = m[6] || (@parser_data['host'] ? @parser_data['host'].match(/:([0-9]*)/).to_a[1] : nil)
	@parser_data[:original_path] = HTTP.decode(m[7], :uri) || '/'
	@parser_data['host'] ||= "#{@parser_data[:host_name]}:#{@parser_data[:port]}"
	# parse query for params - m[9] is the data part of the query
	if m[9]
		HTTP.extract_data m[9].split(/[&;]/), @parser_data[:params]
	end

	HTTP.make_utf8! @parser_data[:original_path]
	@parser_data[:path] = @parser_data[:original_path].chomp('/')
	@parser_data[:original_path].freeze

	HTTP.make_utf8! @parser_data[:host_name] if @parser_data[:host_name]
	HTTP.make_utf8! @parser_data[:query]

	@parser_data[:client_ip] = @parser_data['x-forwarded-for'].to_s.split(/,[\s]?/)[0] || (service.socket.remote_address.ip_address) rescue 'unknown IP'

	@@rack_dictionary.each {|k,v| @parser_data[k] = @parser_data[v]}

	#create request
	request = HTTPRequest.new service
	request.update @parser_data

	#clear current state
	@parser_data.clear
	@parser_body.clear
	@parser_chunk.clear
	@parser_length = 0
	@parser_stage = 0

	#check for server-responses
	case request.request_method
	when "TRACE"
		return true
	when "OPTIONS"
		Anorexic.push_event Proc.new do
			response = HTTPResponse.new request
			response[:Allow] = "GET,HEAD,POST,PUT,DELETE,OPTIONS"
			response["access-control-allow-origin"] = "*"
			response['content-length'] = 0
			response.finish
		end
		return true
	end

	#pass it to the handler or decler error.
	if service && service.handler
		Anorexic.callback service.handler, :on_request, request
	else
		AN.error "No Handler for this HTTP service."
	end
end

#on_connect(service) ⇒ Object

called when connection is initialized.


28
29
# File 'lib/anorexic/server/protocols/http_protocol.rb', line 28

def on_connect service
end

#on_disconnect(service) ⇒ Object

# called when a disconnect is fired # (socket was disconnected / service should be disconnected / shutdown / socket error)


49
50
# File 'lib/anorexic/server/protocols/http_protocol.rb', line 49

def on_disconnect service
end

#on_exception(service, e) ⇒ Object

called when an exception was raised (socket was disconnected / service should be disconnected / shutdown / socket error)


54
55
56
# File 'lib/anorexic/server/protocols/http_protocol.rb', line 54

def on_exception service, e
	Anorexic.error e
end

#on_message(service) ⇒ Object

called when data is recieved.

this method is called within a lock on the service (Mutex) - craeful from double locking.

typically returns an Array with any data not yet processed (to be returned to the in-que)… but here it always processes (or discards) the data.


36
37
38
39
40
41
42
43
44
45
# File 'lib/anorexic/server/protocols/http_protocol.rb', line 36

def on_message(service)
	# parse the request
	@locker.synchronize { parse_message }
	if (@parser_stage == 1) && @parser_data[:version] >= 1.1
		# send 100 continue message????? doesn't work! both Crome and Safari go crazy if this is sent after the request was sent (but before all the packets were recieved... msgs over 1 Mb).
		# Anorexic.push_event Proc.new { Anorexic.info "sending continue signal."; service.send_nonblock "100 Continue\r\n\r\n" }
		# service.send_unsafe_interrupt "100 Continue\r\n\r\n" # causes double lock on service
	end
	true
end

#parse_body(data) ⇒ Object

parses the body of a request.


124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/anorexic/server/protocols/http_protocol.rb', line 124

def parse_body data
	# check for body is needed, if exists and if complete
	if @parser_data["transfer-coding"] == "chunked"
		until data.empty? || data[0].to_s.match(/0(\r)?\n/)
			if @parser_length == 0
				@parser_length = data.to_s.shift.match(/^[a-z0-9A-Z]+/).to_i(16)
				@parser_chunk.clear
			end
			unless @parser_length == 0
				@parser_chunk << data.shift while ( (@parser_length >= @parser_chunk.bytesize) && data[0])
			end
			if @parser_length <= @parser_chunk.bytesize
				@parser_body << @parser_chunk.byteslice(0, @parser_body.bytesize)
				@parser_length = 0
				@parser_chunk.clear
			end
		end
		return false unless data[0].to_s.match(/0(\r)?\n/)
		true until data.empty? || data.shift.match(/^[\r\n]+$/)
		data.shift while data[0].to_s.match /^[\r\n]+$/
	elsif @parser_data["content-length"].to_i
		@parser_length = @parser_data["content-length"].to_i if @parser_length == 0
		@parser_chunk << data.shift while @parser_length > @parser_chunk.bytesize && data[0]
		return false if @parser_length > @parser_chunk.bytesize
		@parser_body = @parser_chunk.byteslice(0, @parser_length)
		@parser_chunk.clear
	else 
		Anorexic.warn 'bad body request - trying to read'
		@parser_body << data.shift while data[0] && !data[0].match(/^[\r\n]+$/)
	end
	# parse body (POST parameters)
	read_body

	# complete request
	complete_request

	#read next request unless data is finished
	return parse_message data unless data.empty?
	true 
end

#parse_head(data) ⇒ Object

parses the head on a request (headers and values).


101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/anorexic/server/protocols/http_protocol.rb', line 101

def parse_head data
	until data[0].nil? || data[0].match(/^[\r\n]+$/)
		m = data.shift.match(/^([^:]*):[\s]*([^\r\n]*)/)
		# move cookies to cookie-jar, all else goes to headers
		case m[1].downcase
		when 'cookie'
			HTTP.extract_data m[2].split(/[;,][\s]?/), @parser_data[:cookies], :uri
		end
		@parser_data[ HTTP.make_utf8!(m[1]).downcase ] ? (@parser_data[ HTTP.make_utf8!(m[1]).downcase ] << ", #{HTTP.make_utf8! m[2]}"): (@parser_data[ HTTP.make_utf8!(m[1]).downcase ] =  HTTP.make_utf8! m[2])
	end
	return false unless data[0]
	data.shift while data[0] && data[0].match(/^[\r\n]+$/)
	if @parser_data["transfer-coding"] || (@parser_data["content-length"] && @parser_data["content-length"].to_i != 0) || @parser_data["content-type"]
		@parser_stage = 2
	else					
		# create request object and hand over to handler
		complete_request
		return parse_message data unless data.empty?
	end
	true
end

#parse_message(data = nil) ⇒ Object

parses incoming data


62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/anorexic/server/protocols/http_protocol.rb', line 62

def parse_message data = nil
	data ||= service.read.to_s.lines.to_a
	# require 'pry'; binding.pry
	if 	@parser_stage == 0
		return false unless parse_method data
	end
	if 	@parser_stage == 1
		return false unless parse_head data
	end
	if 	@parser_stage == 2
		return false unless parse_body data
	end
	true
end

#parse_method(data) ⇒ Object

parses the method request (the first line in the HTTP request).


78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/anorexic/server/protocols/http_protocol.rb', line 78

def parse_method data
	return false unless data[0] && data[0].match(/^#{HTTP_METHODS.join('|')}/)
	@parser_data[:time_recieved] = Time.now
	@parser_data[:params] = {}
	@parser_data[:cookies] = Cookies.new
	@parser_data[:method] = ''
	@parser_data[:query] = ''
	@parser_data[:original_path] = ''
	@parser_data[:path] = ''
	if defined? Rack
		@parser_data['rack.version'] = Rack::VERSION
		@parser_data['rack.multithread'] = true
		@parser_data['rack.multiprocess'] = false
		@parser_data['rack.hijack?'] = false
		@parser_data['rack.logger'] = Anorexic.logger
	end
	@parser_data[:method], @parser_data[:query], @parser_data[:version] = data.shift.split(/[\s]+/)
	@parser_data[:version] = (@parser_data[:version] || 'HTTP/1.1').match(/[0-9\.]+/).to_s.to_f
	data.shift while data[0].to_s.match /^[\r\n]+/
	@parser_stage = 1
end

#read_bodyObject

read the body's data and parse any incoming data.


225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/anorexic/server/protocols/http_protocol.rb', line 225

def read_body
	# parse content
	case @parser_data["content-type"].to_s
	when /x-www-form-urlencoded/
		HTTP.extract_data @parser_body.split(/[&;]/), @parser_data[:params], :uri
	when /multipart\/form-data/
		read_multipart @parser_data, @parser_body
	when /text\/xml/
		# to-do support xml? support json?
		@parser_data[:body] = @parser_body.dup
	when /application\/json/
		@parser_data[:body] = @parser_body.dup
		JSON.parse(HTTP.make_utf8! @parser_data[:body]).each {|k, v| HTTP.add_param_to_hash k, v, @parser_data[:params]}
	else
		@parser_data[:body] = @parser_body.dup
		Anorexic.error "POST body type (#{@parser_data["content-type"]}) cannot be parsed. raw body is kept in the request's data as request[:body]: #{@parser_body}"
	end
end

#read_multipart(headers, part, name_prefix = '') ⇒ Object

parse a mime/multipart body or part.


245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/anorexic/server/protocols/http_protocol.rb', line 245

def read_multipart headers, part, name_prefix = ''
	if headers["content-type"].to_s.match /multipart/
		boundry = headers["content-type"].match(/boundary=([^\s]+)/)[1]
		if headers["content-disposition"].to_s.match /name=/
			if name_prefix.empty?
				name_prefix << HTTP.decode(headers["content-disposition"].to_s.match(/name="([^"]*)"/)[1])
			else
				name_prefix << "[#{HTTP.decode(headers["content-disposition"].to_s.match(/name="([^"]*)"/)[1])}]"
			end
		end
		part.split(/([\r]?\n)?--#{boundry}(--)?[\r]?\n/).each do |p|
			unless p.strip.empty? || p=='--'
				# read headers
				h = {}
				p = p.lines
				while p[0].match(/^[^:]+:[^\r\n]+/) 
					m = p.shift.match(/^([^:]+):[\s]?([^\r\n]+)/)
					h[m[1].downcase] = m[2]
				end
				if p[0].strip.empty?
					p.shift
				else
					Anorexic.error 'Expected empty line after last header - empty line missing.'
				end
				# send headers and body to be read
				read_multipart h, p.join, name_prefix
			end
		end
		return
	end

	# require a part body to exist (data exists) for parsing
	return true if part.to_s.empty?

	# convert part to `charset` if charset is defined?

	if !headers["content-disposition"]
		Anorexic.error "Wrong multipart format with headers: #{headers} and body: #{part}"
		return
	end

	cd = {}

	HTTP.extract_data headers["content-disposition"].match(/[^;];([^\r\n]*)/)[1].split(/[;,][\s]?/), cd, :uri

	name = name_prefix.dup

	if name_prefix.empty?
		name << HTTP.decode(cd[:name][1..-2])
	else
		name << "[#{HTTP.decode(cd[:name][1..-2])}]"
	end
	if headers["content-type"]
		HTTP.add_param_to_hash "#{name}[data]", part, @parser_data[:params]
		HTTP.add_param_to_hash "#{name}[type]", HTTP.make_utf8!(headers["content-type"]), @parser_data[:params]
		cd.each {|k,v|  HTTP.add_param_to_hash "#{name}[#{k.to_s}]", HTTP.make_utf8!(v[1..-2]), @parser_data[:params] unless k == :name}
	else
		HTTP.add_param_to_hash name, HTTP.decode(part, :utf8), @parser_data[:params]
	end
	true
end