Class: Diode::Request
- Inherits:
-
Object
- Object
- Diode::Request
- Defined in:
- lib/diode/request.rb
Instance Attribute Summary collapse
-
#body ⇒ Object
Returns the value of attribute body.
-
#cookies ⇒ Object
Returns the value of attribute cookies.
-
#env ⇒ Object
Returns the value of attribute env.
-
#fields ⇒ Object
Returns the value of attribute fields.
-
#filters ⇒ Object
Returns the value of attribute filters.
-
#headers ⇒ Object
Returns the value of attribute headers.
-
#method ⇒ Object
Returns the value of attribute method.
-
#params ⇒ Object
Returns the value of attribute params.
-
#path ⇒ Object
Returns the value of attribute path.
-
#pattern ⇒ Object
Returns the value of attribute pattern.
-
#remote ⇒ Object
Returns the value of attribute remote.
-
#url ⇒ Object
Returns the value of attribute url.
-
#version ⇒ Object
Returns the value of attribute version.
Class Method Summary collapse
Instance Method Summary collapse
-
#[](k) ⇒ Object
convenience method for extra info.
-
#[]=(k, v) ⇒ Object
convenience method to store extra info on a request.
-
#dataset_records ⇒ Object
Break up a dataset into an array of records (chunks of xml that can be passed to hash_xml).
-
#hash_multipartform(body, boundary) ⇒ Object
parses a multipart/form-data POST body, using the given boundary separator.
-
#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.
-
#initialize(msg) ⇒ Request
constructor
A new instance of Request.
- #multipart_boundary ⇒ Object
-
#no_extra_fields(*list) ⇒ Object
throws a SecurityError if there are any additional fields found not in the list.
-
#no_extra_parameters(*list) ⇒ Object
throws a SecurityError if there are any additional parameters found not in the list.
-
#to_s ⇒ Object
return the request as a raw HTTP string.
- #url_decode(s) ⇒ Object
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
#body ⇒ Object
Returns the value of attribute body.
30 31 32 |
# File 'lib/diode/request.rb', line 30 def body @body end |
#cookies ⇒ Object
Returns the value of attribute cookies.
30 31 32 |
# File 'lib/diode/request.rb', line 30 def @cookies end |
#env ⇒ Object
Returns the value of attribute env.
30 31 32 |
# File 'lib/diode/request.rb', line 30 def env @env end |
#fields ⇒ Object
Returns the value of attribute fields.
30 31 32 |
# File 'lib/diode/request.rb', line 30 def fields @fields end |
#filters ⇒ Object
Returns the value of attribute filters.
30 31 32 |
# File 'lib/diode/request.rb', line 30 def filters @filters end |
#headers ⇒ Object
Returns the value of attribute headers.
30 31 32 |
# File 'lib/diode/request.rb', line 30 def headers @headers end |
#method ⇒ Object
Returns the value of attribute method.
30 31 32 |
# File 'lib/diode/request.rb', line 30 def method @method end |
#params ⇒ Object
Returns the value of attribute params.
30 31 32 |
# File 'lib/diode/request.rb', line 30 def params @params end |
#path ⇒ Object
Returns the value of attribute path.
30 31 32 |
# File 'lib/diode/request.rb', line 30 def path @path end |
#pattern ⇒ Object
Returns the value of attribute pattern.
30 31 32 |
# File 'lib/diode/request.rb', line 30 def pattern @pattern end |
#remote ⇒ Object
Returns the value of attribute remote.
30 31 32 |
# File 'lib/diode/request.rb', line 30 def remote @remote end |
#url ⇒ Object
Returns the value of attribute url.
30 31 32 |
# File 'lib/diode/request.rb', line 30 def url @url end |
#version ⇒ Object
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_records ⇒ Object
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_boundary ⇒ Object
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_s ⇒ Object
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 |