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.



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
74
75
76
# File 'lib/diode/request.rb', line 32

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.



30
31
32
# File 'lib/diode/request.rb', line 30

def body
  @body
end

#cookiesObject

Returns the value of attribute cookies.



30
31
32
# File 'lib/diode/request.rb', line 30

def cookies
  @cookies
end

#envObject

Returns the value of attribute env.



30
31
32
# File 'lib/diode/request.rb', line 30

def env
  @env
end

#fieldsObject

Returns the value of attribute fields.



30
31
32
# File 'lib/diode/request.rb', line 30

def fields
  @fields
end

#filtersObject

Returns the value of attribute filters.



30
31
32
# File 'lib/diode/request.rb', line 30

def filters
  @filters
end

#headersObject

Returns the value of attribute headers.



30
31
32
# File 'lib/diode/request.rb', line 30

def headers
  @headers
end

#methodObject

Returns the value of attribute method.



30
31
32
# File 'lib/diode/request.rb', line 30

def method
  @method
end

#paramsObject

Returns the value of attribute params.



30
31
32
# File 'lib/diode/request.rb', line 30

def params
  @params
end

#pathObject

Returns the value of attribute path.



30
31
32
# File 'lib/diode/request.rb', line 30

def path
  @path
end

#patternObject

Returns the value of attribute pattern.



30
31
32
# File 'lib/diode/request.rb', line 30

def pattern
  @pattern
end

#remoteObject

Returns the value of attribute remote.



30
31
32
# File 'lib/diode/request.rb', line 30

def remote
  @remote
end

#urlObject

Returns the value of attribute url.



30
31
32
# File 'lib/diode/request.rb', line 30

def url
  @url
end

#versionObject

Returns the value of attribute version.



30
31
32
# File 'lib/diode/request.rb', line 30

def version
  @version
end

Class Method Details

.mock(url) ⇒ Object



24
25
26
27
28
# File 'lib/diode/request.rb', line 24

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



79
80
81
# File 'lib/diode/request.rb', line 79

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

#[]=(k, v) ⇒ Object

convenience method to store extra info on a request



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

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.



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

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



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

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 }


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
146
147
148
# File 'lib/diode/request.rb', line 100

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



184
185
186
187
188
189
190
191
192
# File 'lib/diode/request.rb', line 184

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



178
179
180
181
182
# File 'lib/diode/request.rb', line 178

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



171
172
173
174
175
# File 'lib/diode/request.rb', line 171

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



219
220
221
222
223
224
225
226
# File 'lib/diode/request.rb', line 219

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



88
89
90
# File 'lib/diode/request.rb', line 88

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