Class: Diode::Request

Inherits:
Object
  • Object
show all
Defined in:
lib/diode/request.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(msg) ⇒ Request

Returns a new instance of Request.



29
30
31
32
33
34
35
36
37
38
39
40
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
# File 'lib/diode/request.rb', line 29

def initialize(msg)
  reqline, sep, msg = msg.partition("\r\n")
  raise(Diode::RequestError.new(400)) if reqline.to_s.empty?
  raise(Diode::RequestError.new(405)) unless reqline.start_with?("GET ") or reqline.start_with?("POST ")
  raise(Diode::RequestError.new(400)) unless reqline.end_with?(" HTTP/1.0") or reqline.end_with?(" HTTP/1.1")
  @method = reqline.start_with?("GET ") ? "GET" : "POST"
  @version = reqline[-3..-1]
  @url = reqline[(@method.size+1)..-10]
  @path, _sep, @query = @url.partition("?")
  @params = {}
  @fields = {}
  unless @query.nil?
    @query.split("&").each{|pair|
      name, value = pair.split("=")
      next if name.to_s.empty?
      @params[name] = url_decode(value)
    }
  end
  return if msg.nil?
  @headers = {}
  begin
    headerline, sep, msg = msg.partition("\r\n")
    while not headerline.strip.empty?
      key, value = headerline.strip.split(': ')
      key = key.strip.downcase().split(/\b/).collect{|e| e[0].upcase + e[1..-1].downcase}.join("") # title case
      raise(Diode::RequestError.new(400, "duplicate header '#{key}'")) if headers.keys.include?(key)
      @headers[key] = value
      headerline, sep, msg = msg.partition("\r\n")
    end
  rescue EOFError # tolerate missing \r\n at end of request
  end
  @cookies = {}
  if @headers.key?("Cookie")
    @headers["Cookie"].split('; ').each { |c|
      k, eq, v = c.partition("=")
      @cookies[k] = v
    }
  end
  @body = msg
  @fields = {}  # to store fields from JSON or XML body
  @env = {}     # application settings, added by Diode::Server
  @filters = [] # list of filters, set by Diode::Server
  @remote = nil # AddrInfo set by Diode::Server - useful for logging the source IP
  @pattern = %r{^/} # set by Diode::Server - used by Diode::Static
end

Instance Attribute Details

#bodyObject

Returns the value of attribute body.



27
28
29
# File 'lib/diode/request.rb', line 27

def body
  @body
end

#cookiesObject

Returns the value of attribute cookies.



27
28
29
# File 'lib/diode/request.rb', line 27

def cookies
  @cookies
end

#envObject

Returns the value of attribute env.



27
28
29
# File 'lib/diode/request.rb', line 27

def env
  @env
end

#fieldsObject

Returns the value of attribute fields.



27
28
29
# File 'lib/diode/request.rb', line 27

def fields
  @fields
end

#filtersObject

Returns the value of attribute filters.



27
28
29
# File 'lib/diode/request.rb', line 27

def filters
  @filters
end

#headersObject

Returns the value of attribute headers.



27
28
29
# File 'lib/diode/request.rb', line 27

def headers
  @headers
end

#methodObject

Returns the value of attribute method.



27
28
29
# File 'lib/diode/request.rb', line 27

def method
  @method
end

#paramsObject

Returns the value of attribute params.



27
28
29
# File 'lib/diode/request.rb', line 27

def params
  @params
end

#pathObject

Returns the value of attribute path.



27
28
29
# File 'lib/diode/request.rb', line 27

def path
  @path
end

#patternObject

Returns the value of attribute pattern.



27
28
29
# File 'lib/diode/request.rb', line 27

def pattern
  @pattern
end

#remoteObject

Returns the value of attribute remote.



27
28
29
# File 'lib/diode/request.rb', line 27

def remote
  @remote
end

#urlObject

Returns the value of attribute url.



27
28
29
# File 'lib/diode/request.rb', line 27

def url
  @url
end

#versionObject

Returns the value of attribute version.



27
28
29
# File 'lib/diode/request.rb', line 27

def version
  @version
end

Class Method Details

.mock(url) ⇒ Object



21
22
23
24
25
# File 'lib/diode/request.rb', line 21

def self.mock(url)
  u = URI(url)
  msg = "GET #{u.path}#{u.query.nil? ? "" : "?"+u.query} HTTP/1.1\r\nHost: #{u.host}\r\nUser-Agent: MockDiode/1.0\r\n\r\n"
  new(msg)
end

Instance Method Details

#[](k) ⇒ Object

convenience method for extra info



76
77
78
# File 'lib/diode/request.rb', line 76

def [](k)
  @env[k]
end

#[]=(k, v) ⇒ Object

convenience method to store extra info on a request



81
82
83
# File 'lib/diode/request.rb', line 81

def []=(k,v)
  @env[k] = v
end

#dataset_recordsObject

Break up a dataset into an array of records (chunks of xml that can be passed to hash_xml). We insist on dataset/record names for tags.



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/diode/request.rb', line 149

def dataset_records()
  xml=@body.dup()
  pos = xml.index("<dataset")  # find root open tag
  raise(Diode::RequestError.new(400, "invalid xml has no root tag")) if pos.nil?
  xml.slice!(0,pos+8) # discard anything before opening tag name
  return([]) if xml.strip.start_with?("/>")
  pos = xml.index("total=")
  xml.slice!(0,pos+7)  # remove up to number of records
  count = xml[/\d+/].to_i()
  return([]) if count.zero?
  xml.slice!(0, xml.index(">")+1) # remove rest of dataset open tag
  records = xml.split("</record>")
  records.pop() # remove the dataset close tag
  raise(Diode::RequestError.new(400, "records do not match total")) unless records.size == count
  records.collect!{ |r| r+"</record>" }
  records
end

#hash_multipartform(body, boundary) ⇒ Object

parses a multipart/form-data POST body, using the given boundary separator



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/diode/request.rb', line 192

def hash_multipartform(body, boundary)
  # cannot use split on possibly invalid UTF-8, but we can use partition()
  preamble, _, rest = body.partition("--"+boundary)
  raise("preamble before boundary preamble="+preamble.inspect()) unless preamble.empty?
  raise("no multipart ending found, expected --\\r\\n at end") unless rest.end_with?(boundary+"--\r\n")
  until rest == "--\r\n"
    part, _, rest = rest.partition("--"+boundary)
    spec, _, value = part.chomp.partition("\r\n\r\n")
    spec =~ /; name="([^"]+)"/m
    name = $1
    spec =~ /; filename="([^"]+)"/m
    filename = $1
    spec =~ /Content-Type: ([^"]+)/m
    mimetype = $1
    if mimetype.nil?
      @fields[name] = value.force_encoding("UTF-8")
    else
      @fields[name] = {"filename" => filename, "mimetype" => mimetype, "contents" => value}
    end
  end
  @fields
end

#hash_xml(xml = nil) ⇒ Object

Extract fields by reading xml body in a strict format: any single root element, zero or more direct children only, attributes are ignored. A hash is assembled using tagname of child as key and text of child as value. the root may have an “id” attribute which will be treated like a field (named “id”) Anything else is ignored. For example:

<anything id="13"><firstname>john</firstname><age>25</age></anything>

becomes:

Hash { "id" => 13, "firstname" => "john", "age" => 25 }


97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/diode/request.rb', line 97

def hash_xml(xml=nil)
  xml ||= @body.dup()
  pos = xml.index("<")  # find root open tag
  raise(Diode::RequestError.new(400, "invalid xml has no open tag")) if pos.nil?
  xml.slice!(0,pos+1) # discard anything before opening tag name
  pos = xml.index(">")
  rootelement = xml.slice!(0, pos) # we might have "root" or 'root recordid="12345"'
  xml.slice!(0,1) # remove the closing bracket of root element
  pos = rootelement.index(" ")
  if pos.nil?
    roottag = rootelement
  else
    roottag = rootelement.slice(0,pos)
    rest = /id="([^"]+)"/.match(rootelement[pos..-1])
    @fields["id"] = rest[1] unless rest.nil? or rest.size < 2
  end
  raise(Diode::RequestError.new(400, "invalid root open tag")) if roottag.nil? or /\A[a-z][a-z0-9]+\z/.match(roottag).nil?
  ending = xml.slice!(/\<\/#{roottag}\>.*$/m)
  raise(Diode::RequestError.new(400, "invalid root close tag")) if ending.nil? # discard everything after close
  # now we have a list of items like: \t<tagname>value</tagname>\n or maybe <tagname />
  until xml.empty?
    # find a field tagname
    pos = xml.index("<")
    break if pos.nil?
    xml.slice!(0,pos+1) # discard anything before opening tag name
    pos = xml.index(">")
    raise(Diode::RequestError.new(400, "invalid field open tag")) if pos.nil?
    if pos >= 2 and xml[pos-1] == "/"  # we have a self-closed tag eg. <first updated="true"/>
      tagelement = xml.slice!(0, pos+1)[0..-3] # tagname plus maybe attributes
      pos = tagelement.index(" ")
      tagname = (pos.nil?) ? tagelement : tagelement.slice(0,pos) # ignore attributes on fields
      raise(Diode::RequestError.new(400, "invalid field open tag")) if tagname.nil? or /\A[a-z][a-z0-9]+\z/.match(tagname).nil?
      @fields[tagname] = ""
    else # eg. <first updated="true" >some value </first>\n
      tagelement = xml.slice!(0, pos)
      pos = tagelement.index(" ")
      tagname = (pos.nil?) ? tagelement : tagelement.slice(0,pos) # ignore attributes on fields
      raise(Diode::RequestError.new(400, "invalid field open tag")) if tagname.nil? or /\A[a-z][a-z0-9]+\z/.match(tagname).nil?
      raise(Diode::RequestError.new(400, "duplicate field is not permitted")) if @fields.key?(tagname)
      xml.slice!(0,1) # remove closing bracket
      pos = xml.index("</#{tagname}>") # demand strict syntax for closing tag
      raise(Diode::RequestError.new(400, "no closing tag")) if pos.nil?
      raise(Diode::RequestError.new(400, "field value too long")) unless pos < 2048 # no field values 2048 bytes or larger
      value = xml.slice!(0,pos)
      @fields[tagname] = value
      xml.slice!(0, "</#{tagname}>".size)
    end
  end
end

#multipart_boundaryObject



181
182
183
184
185
186
187
188
189
# File 'lib/diode/request.rb', line 181

def multipart_boundary()
  spec = "multipart/form-data; boundary="
  contentType = @headers["Content-Type"]
  if contentType.start_with?(spec)
    return(contentType.chomp.sub(spec, "").force_encoding("utf-8"))
  else
    return ""
  end
end

#no_extra_fields(*list) ⇒ Object

throws a SecurityError if there are any additional fields found not in the list



175
176
177
178
179
# File 'lib/diode/request.rb', line 175

def no_extra_fields(*list)
  kill = @fields.keys()
  list.each { |k| kill.delete(k) }
  raise(Diode::SecurityError, "extra fields #{kill}") unless kill.empty?
end

#no_extra_parameters(*list) ⇒ Object

throws a SecurityError if there are any additional parameters found not in the list



168
169
170
171
172
# File 'lib/diode/request.rb', line 168

def no_extra_parameters(*list)
  kill = @params.keys()
  list.each { |param| kill.delete(param) }
  raise(Diode::SecurityError, "extra parameters #{kill}") unless kill.empty?
end

#to_sObject

return the request as a raw HTTP string



216
217
218
219
220
221
222
223
# File 'lib/diode/request.rb', line 216

def to_s()
  @headers["Content-Length"] = @body.bytes.size() unless @body.empty?
  msg = ["#{@method} #{@url} HTTP/1.1"]
  @headers.keys.each { |k|
    msg << "#{k}: #{@headers[k]}"
  }
  msg.join("\r\n") + "\r\n\r\n" + @body
end

#url_decode(s) ⇒ Object



85
86
87
# File 'lib/diode/request.rb', line 85

def url_decode(s)
  s.to_s.b.tr('+', ' ').gsub(/\%([A-Za-z0-9]{2})/) {[$1].pack("H2")}.force_encoding(Encoding::UTF_8)
end