Module: Nut::Request

Defined in:
lib/nut/request.rb

Overview

Request Module

Constant Summary collapse

CTYPE_REX =

Content-Type Regex

/^[^;]+/
BOUNDARY_REX =

Boundary Regex

/boundary=(.+)$/
MULTIPART_FIELD_REX =

Multipart Field Regex

/^([^=]+)=(.+)$/
EXPECT_CONTINUE_REX =

Expect: 100-continue

/100.continue/i

Class Method Summary collapse

Class Method Details

.build_body_chunked(creq, msg) ⇒ Object

Build Body - Chunked: Assembles Chunks from msg (if Headers are complete) and assembles Request Body from them.

Parameters:

  • creq (Hash)

    Client Request Context

  • msg (String)

    A line of text to be processed



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
# File 'lib/nut/request.rb', line 174

def self.build_body_chunked creq, msg

	# Check for Chunked Transfer-Encoding
	return unless is_chunked? creq

	# Are we already reading a chunk?
	if creq[:chunk][:size]

		# Pull Chunk Data
		creq[:chunk][:data] ||= ''
		creq[:chunk][:data] << msg.chomp

		# Check Chunk Complete
		if creq[:chunk][:data].bytes.size >= creq[:chunk][:size]

			# Append to Body
			creq[:req_b] << creq[:chunk][:data].bytes.slice(0, creq[:chunk][:size]).pack('c*')

			# Reset Chunk
			creq[:chunk] = {}
		end
	else

		# Acquire next chunk size
		creq[:chunk][:size] = /[0-9]/.match(msg.chomp).try(:[], 0).try :to_i

		# Check Body Complete
		creq[:body_complete] = true if creq[:chunk][:size] == 0
	end
end

.build_body_normal(creq, msg) ⇒ Object

Build Body - Normal (Content-Length): Assembles Request Body from msg if Headers are complete, until Content-Length is reached.

Parameters:

  • creq (Hash)

    Client Request Context

  • msg (String)

    A line of text to be processed



209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/nut/request.rb', line 209

def self.build_body_normal creq, msg

	# Check for Content-Length
	return unless is_content_len? creq

	# Reject first empty line (except for multipart requests, in which the extra CRLF helps in splitting up request parts)
	if creq[:got_first_line] || (creq[:ctype] == 'multipart/form-data')

		# Assemble Request Body
		creq[:req_b] << msg

		# Check Body Complete
		if creq[:req_b].bytesize >= creq[:headz][:content_length].to_i
			creq[:body_complete] = true
			creq[:req_b] = creq[:req_b].bytes.slice(0, creq[:headz][:content_length].to_i).pack 'c*'
		end
	else
		creq[:got_first_line] = true
	end
end

.build_gen_head(lines) ⇒ Hash

Build Generic Headers: Constructs a structured Header Hash from an Array of text lines.

Parameters:

  • lines (Array)

    A bunch of lines containing header data

Returns:

  • (Hash)

    A Hash representation of the Headers



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/nut/request.rb', line 135

def self.build_gen_head lines

	# Prepare Header
	headz = {}

	# Pull up Header lines as { "Field-Name" => [val0, val1, ..., valX] }
	lines
		.inject([]) { |a, l| if /^[ \t]+/ =~ l then a.last << l.gsub(/^[ \t]+/, ' ') else a << l end; a }
		.collect { |l| l.chomp.split ': ', 2 }
		.select { |name, _val| name }
		.each { |name, val| headz[name] ||= []; headz[name] << val }

	# Concat Header fields into { field_name: 'field0,field1,...,fieldX' }
	Hash[*(headz.inject([]) { |a, h| a + [h.first.downcase.gsub('-', '_').to_sym, h.last.join(',')] })]
end

.build_header(creq, msg) ⇒ Object

Build Header: Extracts HTTP Headers from Request if available and not already set, returning true on success, false otherwise.

Parameters:

  • creq (Hash)

    Client Request Context

  • msg (String)

    A line of text to be processed



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/nut/request.rb', line 115

def self.build_header creq, msg

	# Pull Headers if not already done
	if msg.chomp == '' && creq[:headz].empty?

		# Build Headers
		creq[:headz] = build_gen_head creq[:lines].dclone.slice(1, creq[:lines].size)

		# Acquire Clean Content-Type
		creq[:ctype] = CTYPE_REX.match(creq[:headz][:content_type]).try :[], 0

		# Respond to 100-continue Requests
		Nut::Handler.write creq[:client], "HTTP/1.1 100 Continue\r\n\r\n" if EXPECT_CONTINUE_REX =~ creq[:headz][:expect]
	end
end

.build_no_body(creq) ⇒ Object

Build without Body: Marks Request Body as Complete if no Body should be present (no content-length & no chunked encoding).

Parameters:

  • creq (Hash)

    Client Request Context



233
234
235
236
237
238
239
240
# File 'lib/nut/request.rb', line 233

def self.build_no_body creq

	# Check whether Body should be present
	return if expects_body? creq

	# Mark No Body
	creq[:body_complete] = true
end

.build_req_body(creq, msg) ⇒ Object

Build Request Body: Builds Request Body according to Request Headers (Content-Length / Transfer-Encoding).

Parameters:

  • creq (Hash)

    Client Request Context

  • msg (String)

    A line of text to be processed



155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/nut/request.rb', line 155

def self.build_req_body creq, msg

	# Check for Headers
	return if creq[:headz].empty?

	# Handle Chunked Transfer-Encoding
	build_body_chunked creq, msg

	# Handle Simple Body
	build_body_normal creq, msg

	# Handle No Body
	build_no_body creq
end

.build_req_line(creq, msg) ⇒ Object

Build Request Line: Parses msg as the Request Line unless already set, extracting HTTP Verb, URI & Version.

Parameters:

  • creq (Hash)

    Client Request Context

  • msg (String)

    A line of text to be processed



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/nut/request.rb', line 95

def self.build_req_line creq, msg

	# Check Request Line
	return if creq[:req_l]

	# Set Request Line
	rl = msg.chomp
	creq[:req_l] = rl

	# Pull up Verb, URI, Version
	creq[:verb] = rl.slice 0, rl.index(' ')
	creq[:uri] = rl.slice rl.index(' ') + 1, rl.size
	creq[:version] = creq[:uri].slice! creq[:uri].rindex(' ') + 1, creq[:uri].size
	creq[:uri].chomp! ' '
end

.expects_body?(creq) ⇒ Boolean

Expects Body?: Determines whether request expects a body (according to headers).

Parameters:

  • creq (Hash)

    Client Request Context

Returns:

  • (Boolean)


262
263
264
# File 'lib/nut/request.rb', line 262

def self.expects_body? creq
	is_content_len?(creq) || is_chunked?(creq)
end

.extract_multipart_form_data(req) ⇒ Object

Extract Multipart Form Data: Extracts Multipart Form Data from the Request Body and merges it into req[:params].

Parameters:

  • req (Hash)

    Request Hash



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/nut/request.rb', line 41

def self.extract_multipart_form_data req

	# Acquire Boundary
	req[:boundary] = BOUNDARY_REX.match(req[:headers][:content_type]).try(:[], 1).try :gsub, /[ \t]*$/, ''

	# Split Body into Parts
	req[:parts] = req[:body].split("\r\n--#{req[:boundary]}").reject { |p| p.empty? }

	# Remove last part ('--')
	req[:parts].pop

	# Collect Form Data from Parts
	req[:parts].each do |p|

		# Split Headers & Data
		headers, data = p.split "\r\n\r\n", 2

		# Generate Headers
		headers = build_gen_head headers.split("\r\n")

		# Extract additional information from headers
		finfo = Hash[*(headers[:content_disposition].split('; ').inject([]) do |a, f|
			m = MULTIPART_FIELD_REX.match(f)
			if m
				name = m.try :[], 1
				val = m.try :[], 2
				val = val.slice(1, val.size - 2) if val.start_with?('"') && val.end_with?('"')
				a + [name.downcase.to_sym, val]
			else
				a
			end
		end)]

		# Merge Request Params
		req[:params][finfo[:name].to_sym] = finfo[:filename] ? { filename: finfo[:filename], data: data } : data
	end
end

.extract_req_data(req) ⇒ Object

Pull Request Details: Extracts any possible Form Data and extended Request details from a Request Hash.

Parameters:

  • req (Hash)

    Request Hash



25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/nut/request.rb', line 25

def self.extract_req_data req

	# Switch on Content-Type { Extract Form Data }
	case req[:ctype]
		when 'multipart/form-data'
			extract_multipart_form_data req
		when 'application/x-www-form-urlencoded'
			extract_urlencoded_form_data req
		else
			# NoOp
	end
end

.extract_urlencoded_form_data(req) ⇒ Object

Extract URL-Encoded Form Data: Extracts URL-Encoded Form Data from the Request Body and merges it into req[:params].

Parameters:

  • req (Hash)

    Request Hash



82
83
84
85
86
87
88
89
# File 'lib/nut/request.rb', line 82

def self.extract_urlencoded_form_data req

	# Split Body into Params & Merge
	req[:body]
		.split('&')
		.collect { |p| p.split '=', 2 }
		.each { |name, val| req[:params][name] = val }
end

.is_chunked?(creq) ⇒ Boolean

Is Chunked?: Determines whether request is chunked (as per transfer-encoding header).

Parameters:

  • creq (Hash)

    Client Request Context

Returns:

  • (Boolean)


254
255
256
# File 'lib/nut/request.rb', line 254

def self.is_chunked? creq
	creq[:headz].try(:[], :transfer_encoding) && (creq[:headz].try(:[], :transfer_encoding) != 'identity')
end

.is_content_len?(creq) ⇒ Boolean

Is Content-Length?: Determines whether request is bound by a content-length header.

Parameters:

  • creq (Hash)

    Client Request Context

Returns:

  • (Boolean)


246
247
248
# File 'lib/nut/request.rb', line 246

def self.is_content_len? creq
	creq[:headz].try(:[], :content_length) && (!(creq[:headz].try(:[], :transfer_encoding)) || (creq[:headz].try(:[], :transfer_encoding) == 'identity'))
end