diode 1.2.2 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 323c1d5146b6e56105dd9bf4586ef95d8b6a6b41263ef6c7c18e5d8da537b60f
4
- data.tar.gz: 72f818e9086fa61a118432127bf5966139f004c424f95b1fffa3cf73dde37337
3
+ metadata.gz: b8b71e839db24b48761cbd168c49dcd69ba03c9eba9033738f3b6998561dff8f
4
+ data.tar.gz: d9fd0af9f07501b19c534fd9209973ad07002b9aef0ec5c00d3163f71f3e3e93
5
5
  SHA512:
6
- metadata.gz: a267eaf498f643f2082bd32acdfe84628de258804e9965b34a4ee42634f4d9cc74dbe6686e09a21d6c2ab04ce154f1167c73086e053d474470e60eba8b212816
7
- data.tar.gz: a287277870c88a0aeb27e76508a51b5470355e9ac22f7821ed85e5102b269a3306631d0e876943c4dfbc824226aafdf09d722da59fa014cd71c0b5c8a5995e98
6
+ metadata.gz: 24da9787c6334cc86c2a586ba05a6330c6640279ae886312d2a480b05477c1a85c80c61366f9fd4ccf458d0d3a6aa236ee846311146d3a2f2274c6cdd2904db2
7
+ data.tar.gz: 443d99195550acad9c33740f0f5276c019341824f969a895bcbba69ada0b84324a2de4dea7bb5d57dfc8d2d72f2bf5a968beeb64cf4d5e593437c7c26cf2b4e9
data/lib/diode/request.rb CHANGED
@@ -3,14 +3,14 @@ require 'uri'
3
3
  module Diode
4
4
 
5
5
  class RequestError < StandardError
6
- attr_accessor(:code)
7
- def initialize(code, message="")
8
- @code = code
9
- super(message)
10
- end
11
- def ==(e) # define equality for convenient testing
12
- message == e.message
13
- end
6
+ attr_accessor(:code)
7
+ def initialize(code, message="")
8
+ @code = code
9
+ super(message)
10
+ end
11
+ def ==(e) # define equality for convenient testing
12
+ message == e.message
13
+ end
14
14
  end
15
15
 
16
16
  class SecurityError < StandardError
@@ -18,208 +18,208 @@ end
18
18
 
19
19
  class Request
20
20
 
21
- def self.mock(url)
22
- u = URI(url)
23
- 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"
24
- new(msg)
25
- end
26
-
27
- attr_accessor(:method, :version, :url, :path, :params, :headers, :cookies, :body, :fields, :env, :filters, :remote, :pattern)
28
-
29
- def initialize(msg)
30
- reqline, sep, msg = msg.partition("\r\n")
31
- raise(Diode::RequestError.new(400)) if reqline.to_s.empty?
32
- raise(Diode::RequestError.new(405)) unless reqline.start_with?("GET ") or reqline.start_with?("POST ")
33
- raise(Diode::RequestError.new(400)) unless reqline.end_with?(" HTTP/1.0") or reqline.end_with?(" HTTP/1.1")
34
- @method = reqline.start_with?("GET ") ? "GET" : "POST"
35
- @version = reqline[-3..-1]
36
- @url = reqline[(@method.size+1)..-10]
37
- @path, _sep, @query = @url.partition("?")
38
- @params = {}
39
- @fields = {}
40
- unless @query.nil?
41
- @query.split("&").each{|pair|
42
- name, value = pair.split("=")
43
- next if name.to_s.empty?
44
- @params[name] = url_decode(value)
45
- }
46
- end
47
- return if msg.nil?
48
- @headers = {}
49
- begin
50
- headerline, sep, msg = msg.partition("\r\n")
51
- while not headerline.strip.empty?
52
- key, value = headerline.strip.split(': ')
53
- @headers[key] = value
54
- headerline, sep, msg = msg.partition("\r\n")
55
- end
56
- rescue EOFError
57
- # tolerate missing \r\n at end of request
58
- end
59
- @cookies = {}
60
- if @headers.key?("Cookie")
61
- @headers["Cookie"].split('; ').each { |c|
62
- k, eq, v = c.partition("=")
63
- @cookies[k] = v
64
- }
65
- end
66
- @body = msg
67
- @fields = {} # to store fields from JSON or XML body
68
- @env = {} # application settings, added by Diode::Server
69
- @filters = [] # list of filters, set by Diode::Server
70
- @remote = nil # AddrInfo set by Diode::Server - useful for logging the source IP
71
- @pattern = %r{^/} # set by Diode::Server - used by Diode::Static
72
- end
73
-
74
- # convenience method for extra info
75
- def [](k)
76
- @env[k]
77
- end
78
-
79
- # convenience method to store extra info on a request
80
- def []=(k,v)
81
- @env[k] = v
82
- end
83
-
84
- def url_decode(s)
85
- s.to_s.b.tr('+', ' ').gsub(/\%([A-Za-z0-9]{2})/) {[$1].pack("H2")}.force_encoding(Encoding::UTF_8)
86
- end
87
-
88
- # Extract fields by reading xml body in a strict format: any single root element, zero or more direct children only,
89
- # attributes are ignored. A hash is assembled using tagname of child as key and text of child as value.
90
- # the root may have an "id" attribute which will be treated like a field (named "id")
91
- # Anything else is ignored.
92
- # For example:
93
- # <anything id="13"><firstname>john</firstname><age>25</age></anything>
94
- # becomes:
95
- # Hash { "id" => 13, "firstname" => "john", "age" => 25 }
96
- def hash_xml(xml=nil)
97
- xml ||= @body.dup()
98
- pos = xml.index("<") # find root open tag
99
- raise(Diode::RequestError.new(400, "invalid xml has no open tag")) if pos.nil?
100
- xml.slice!(0,pos+1) # discard anything before opening tag name
101
- pos = xml.index(">")
102
- rootelement = xml.slice!(0, pos) # we might have "root" or 'root recordid="12345"'
103
- xml.slice!(0,1) # remove the closing bracket of root element
104
- pos = rootelement.index(" ")
105
- if pos.nil?
106
- roottag = rootelement
107
- else
108
- roottag = rootelement.slice(0,pos)
109
- rest = /id="([^"]+)"/.match(rootelement[pos..-1])
110
- @fields["id"] = rest[1] unless rest.nil? or rest.size < 2
111
- end
112
- raise(Diode::RequestError.new(400, "invalid root open tag")) if roottag.nil? or /\A[a-z][a-z0-9]+\z/.match(roottag).nil?
113
- ending = xml.slice!(/\<\/#{roottag}\>.*$/m)
114
- raise(Diode::RequestError.new(400, "invalid root close tag")) if ending.nil? # discard everything after close
115
- # now we have a list of items like: \t<tagname>value</tagname>\n or maybe <tagname />
116
- until xml.empty?
117
- # find a field tagname
118
- pos = xml.index("<")
119
- break if pos.nil?
120
- xml.slice!(0,pos+1) # discard anything before opening tag name
121
- pos = xml.index(">")
122
- raise(Diode::RequestError.new(400, "invalid field open tag")) if pos.nil?
123
- if pos >= 2 and xml[pos-1] == "/" # we have a self-closed tag eg. <first updated="true"/>
124
- tagelement = xml.slice!(0, pos+1)[0..-3] # tagname plus maybe attributes
125
- pos = tagelement.index(" ")
126
- tagname = (pos.nil?) ? tagelement : tagelement.slice(0,pos) # ignore attributes on fields
127
- raise(Diode::RequestError.new(400, "invalid field open tag")) if tagname.nil? or /\A[a-z][a-z0-9]+\z/.match(tagname).nil?
128
- @fields[tagname] = ""
129
- else # eg. <first updated="true" >some value </first>\n
130
- tagelement = xml.slice!(0, pos)
131
- pos = tagelement.index(" ")
132
- tagname = (pos.nil?) ? tagelement : tagelement.slice(0,pos) # ignore attributes on fields
133
- raise(Diode::RequestError.new(400, "invalid field open tag")) if tagname.nil? or /\A[a-z][a-z0-9]+\z/.match(tagname).nil?
134
- raise(Diode::RequestError.new(400, "duplicate field is not permitted")) if @fields.key?(tagname)
135
- xml.slice!(0,1) # remove closing bracket
136
- pos = xml.index("</#{tagname}>") # demand strict syntax for closing tag
137
- raise(Diode::RequestError.new(400, "no closing tag")) if pos.nil?
138
- raise(Diode::RequestError.new(400, "field value too long")) unless pos < 2048 # no field values 2048 bytes or larger
139
- value = xml.slice!(0,pos)
140
- @fields[tagname] = value
141
- xml.slice!(0, "</#{tagname}>".size)
142
- end
143
- end
144
- end
145
-
146
- # Break up a dataset into an array of records (chunks of xml that can be passed to hash_xml).
147
- # We insist on dataset/record names for tags.
148
- def dataset_records()
149
- xml=@body.dup()
150
- pos = xml.index("<dataset") # find root open tag
151
- raise(Diode::RequestError.new(400, "invalid xml has no root tag")) if pos.nil?
152
- xml.slice!(0,pos+8) # discard anything before opening tag name
153
- return([]) if xml.strip.start_with?("/>")
154
- pos = xml.index("total=")
155
- xml.slice!(0,pos+7) # remove up to number of records
156
- count = xml[/\d+/].to_i()
157
- return([]) if count.zero?
158
- xml.slice!(0, xml.index(">")+1) # remove rest of dataset open tag
159
- records = xml.split("</record>")
160
- records.pop() # remove the dataset close tag
161
- raise(Diode::RequestError.new(400, "records do not match total")) unless records.size == count
162
- records.collect!{ |r| r+"</record>" }
163
- records
164
- end
165
-
166
- # throws a SecurityError if there are any additional parameters found not in the list
167
- def no_extra_parameters(*list)
168
- kill = @params.keys()
169
- list.each { |param| kill.delete(param) }
170
- raise(Diode::SecurityError, "extra parameters #{kill}") unless kill.empty?
171
- end
172
-
173
- # throws a SecurityError if there are any additional fields found not in the list
174
- def no_extra_fields(*list)
175
- kill = @fields.keys()
176
- list.each { |k| kill.delete(k) }
177
- raise(Diode::SecurityError, "extra fields #{kill}") unless kill.empty?
178
- end
179
-
180
- def multipart_boundary()
181
- spec = "multipart/form-data; boundary="
182
- contentType = @headers["Content-Type"]
183
- if contentType.start_with?(spec)
184
- return(contentType.chomp.sub(spec, "").force_encoding("utf-8"))
185
- else
186
- return ""
187
- end
188
- end
189
-
190
- # parses a multipart/form-data POST body, using the given boundary separator
191
- def hash_multipartform(body, boundary)
192
- # cannot use split on possibly invalid UTF-8, but we can use partition()
193
- preamble, _, rest = body.partition("--"+boundary)
194
- raise("preamble before boundary preamble="+preamble.inspect()) unless preamble.empty?
195
- raise("no multipart ending found, expected --\\r\\n at end") unless rest.end_with?(boundary+"--\r\n")
196
- until rest == "--\r\n"
197
- part, _, rest = rest.partition("--"+boundary)
198
- spec, _, value = part.chomp.partition("\r\n\r\n")
199
- spec =~ /; name="([^"]+)"/m
200
- name = $1
201
- spec =~ /; filename="([^"]+)"/m
202
- filename = $1
203
- spec =~ /Content-Type: ([^"]+)/m
204
- mimetype = $1
205
- if mimetype.nil?
206
- @fields[name] = value.force_encoding("UTF-8")
207
- else
208
- @fields[name] = {"filename" => filename, "mimetype" => mimetype, "contents" => value}
209
- end
210
- end
211
- @fields
212
- end
213
-
214
- # return the request as a raw HTTP string
215
- def to_s()
216
- @headers["Content-Length"] = @body.bytes.size() unless @body.empty?
217
- msg = ["#{@method} #{@url} HTTP/1.1"]
218
- @headers.keys.each { |k|
219
- msg << "#{k}: #{@headers[k]}"
220
- }
221
- msg.join("\r\n") + "\r\n\r\n" + @body
222
- end
21
+ def self.mock(url)
22
+ u = URI(url)
23
+ 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"
24
+ new(msg)
25
+ end
26
+
27
+ attr_accessor(:method, :version, :url, :path, :params, :headers, :cookies, :body, :fields, :env, :filters, :remote, :pattern)
28
+
29
+ def initialize(msg)
30
+ reqline, sep, msg = msg.partition("\r\n")
31
+ raise(Diode::RequestError.new(400)) if reqline.to_s.empty?
32
+ raise(Diode::RequestError.new(405)) unless reqline.start_with?("GET ") or reqline.start_with?("POST ")
33
+ raise(Diode::RequestError.new(400)) unless reqline.end_with?(" HTTP/1.0") or reqline.end_with?(" HTTP/1.1")
34
+ @method = reqline.start_with?("GET ") ? "GET" : "POST"
35
+ @version = reqline[-3..-1]
36
+ @url = reqline[(@method.size+1)..-10]
37
+ @path, _sep, @query = @url.partition("?")
38
+ @params = {}
39
+ @fields = {}
40
+ unless @query.nil?
41
+ @query.split("&").each{|pair|
42
+ name, value = pair.split("=")
43
+ next if name.to_s.empty?
44
+ @params[name] = url_decode(value)
45
+ }
46
+ end
47
+ return if msg.nil?
48
+ @headers = {}
49
+ begin
50
+ headerline, sep, msg = msg.partition("\r\n")
51
+ while not headerline.strip.empty?
52
+ key, value = headerline.strip.split(': ')
53
+ @headers[key] = value
54
+ headerline, sep, msg = msg.partition("\r\n")
55
+ end
56
+ rescue EOFError
57
+ # tolerate missing \r\n at end of request
58
+ end
59
+ @cookies = {}
60
+ if @headers.key?("Cookie")
61
+ @headers["Cookie"].split('; ').each { |c|
62
+ k, eq, v = c.partition("=")
63
+ @cookies[k] = v
64
+ }
65
+ end
66
+ @body = msg
67
+ @fields = {} # to store fields from JSON or XML body
68
+ @env = {} # application settings, added by Diode::Server
69
+ @filters = [] # list of filters, set by Diode::Server
70
+ @remote = nil # AddrInfo set by Diode::Server - useful for logging the source IP
71
+ @pattern = %r{^/} # set by Diode::Server - used by Diode::Static
72
+ end
73
+
74
+ # convenience method for extra info
75
+ def [](k)
76
+ @env[k]
77
+ end
78
+
79
+ # convenience method to store extra info on a request
80
+ def []=(k,v)
81
+ @env[k] = v
82
+ end
83
+
84
+ def url_decode(s)
85
+ s.to_s.b.tr('+', ' ').gsub(/\%([A-Za-z0-9]{2})/) {[$1].pack("H2")}.force_encoding(Encoding::UTF_8)
86
+ end
87
+
88
+ # Extract fields by reading xml body in a strict format: any single root element, zero or more direct children only,
89
+ # attributes are ignored. A hash is assembled using tagname of child as key and text of child as value.
90
+ # the root may have an "id" attribute which will be treated like a field (named "id")
91
+ # Anything else is ignored.
92
+ # For example:
93
+ # <anything id="13"><firstname>john</firstname><age>25</age></anything>
94
+ # becomes:
95
+ # Hash { "id" => 13, "firstname" => "john", "age" => 25 }
96
+ def hash_xml(xml=nil)
97
+ xml ||= @body.dup()
98
+ pos = xml.index("<") # find root open tag
99
+ raise(Diode::RequestError.new(400, "invalid xml has no open tag")) if pos.nil?
100
+ xml.slice!(0,pos+1) # discard anything before opening tag name
101
+ pos = xml.index(">")
102
+ rootelement = xml.slice!(0, pos) # we might have "root" or 'root recordid="12345"'
103
+ xml.slice!(0,1) # remove the closing bracket of root element
104
+ pos = rootelement.index(" ")
105
+ if pos.nil?
106
+ roottag = rootelement
107
+ else
108
+ roottag = rootelement.slice(0,pos)
109
+ rest = /id="([^"]+)"/.match(rootelement[pos..-1])
110
+ @fields["id"] = rest[1] unless rest.nil? or rest.size < 2
111
+ end
112
+ raise(Diode::RequestError.new(400, "invalid root open tag")) if roottag.nil? or /\A[a-z][a-z0-9]+\z/.match(roottag).nil?
113
+ ending = xml.slice!(/\<\/#{roottag}\>.*$/m)
114
+ raise(Diode::RequestError.new(400, "invalid root close tag")) if ending.nil? # discard everything after close
115
+ # now we have a list of items like: \t<tagname>value</tagname>\n or maybe <tagname />
116
+ until xml.empty?
117
+ # find a field tagname
118
+ pos = xml.index("<")
119
+ break if pos.nil?
120
+ xml.slice!(0,pos+1) # discard anything before opening tag name
121
+ pos = xml.index(">")
122
+ raise(Diode::RequestError.new(400, "invalid field open tag")) if pos.nil?
123
+ if pos >= 2 and xml[pos-1] == "/" # we have a self-closed tag eg. <first updated="true"/>
124
+ tagelement = xml.slice!(0, pos+1)[0..-3] # tagname plus maybe attributes
125
+ pos = tagelement.index(" ")
126
+ tagname = (pos.nil?) ? tagelement : tagelement.slice(0,pos) # ignore attributes on fields
127
+ raise(Diode::RequestError.new(400, "invalid field open tag")) if tagname.nil? or /\A[a-z][a-z0-9]+\z/.match(tagname).nil?
128
+ @fields[tagname] = ""
129
+ else # eg. <first updated="true" >some value </first>\n
130
+ tagelement = xml.slice!(0, pos)
131
+ pos = tagelement.index(" ")
132
+ tagname = (pos.nil?) ? tagelement : tagelement.slice(0,pos) # ignore attributes on fields
133
+ raise(Diode::RequestError.new(400, "invalid field open tag")) if tagname.nil? or /\A[a-z][a-z0-9]+\z/.match(tagname).nil?
134
+ raise(Diode::RequestError.new(400, "duplicate field is not permitted")) if @fields.key?(tagname)
135
+ xml.slice!(0,1) # remove closing bracket
136
+ pos = xml.index("</#{tagname}>") # demand strict syntax for closing tag
137
+ raise(Diode::RequestError.new(400, "no closing tag")) if pos.nil?
138
+ raise(Diode::RequestError.new(400, "field value too long")) unless pos < 2048 # no field values 2048 bytes or larger
139
+ value = xml.slice!(0,pos)
140
+ @fields[tagname] = value
141
+ xml.slice!(0, "</#{tagname}>".size)
142
+ end
143
+ end
144
+ end
145
+
146
+ # Break up a dataset into an array of records (chunks of xml that can be passed to hash_xml).
147
+ # We insist on dataset/record names for tags.
148
+ def dataset_records()
149
+ xml=@body.dup()
150
+ pos = xml.index("<dataset") # find root open tag
151
+ raise(Diode::RequestError.new(400, "invalid xml has no root tag")) if pos.nil?
152
+ xml.slice!(0,pos+8) # discard anything before opening tag name
153
+ return([]) if xml.strip.start_with?("/>")
154
+ pos = xml.index("total=")
155
+ xml.slice!(0,pos+7) # remove up to number of records
156
+ count = xml[/\d+/].to_i()
157
+ return([]) if count.zero?
158
+ xml.slice!(0, xml.index(">")+1) # remove rest of dataset open tag
159
+ records = xml.split("</record>")
160
+ records.pop() # remove the dataset close tag
161
+ raise(Diode::RequestError.new(400, "records do not match total")) unless records.size == count
162
+ records.collect!{ |r| r+"</record>" }
163
+ records
164
+ end
165
+
166
+ # throws a SecurityError if there are any additional parameters found not in the list
167
+ def no_extra_parameters(*list)
168
+ kill = @params.keys()
169
+ list.each { |param| kill.delete(param) }
170
+ raise(Diode::SecurityError, "extra parameters #{kill}") unless kill.empty?
171
+ end
172
+
173
+ # throws a SecurityError if there are any additional fields found not in the list
174
+ def no_extra_fields(*list)
175
+ kill = @fields.keys()
176
+ list.each { |k| kill.delete(k) }
177
+ raise(Diode::SecurityError, "extra fields #{kill}") unless kill.empty?
178
+ end
179
+
180
+ def multipart_boundary()
181
+ spec = "multipart/form-data; boundary="
182
+ contentType = @headers["Content-Type"]
183
+ if contentType.start_with?(spec)
184
+ return(contentType.chomp.sub(spec, "").force_encoding("utf-8"))
185
+ else
186
+ return ""
187
+ end
188
+ end
189
+
190
+ # parses a multipart/form-data POST body, using the given boundary separator
191
+ def hash_multipartform(body, boundary)
192
+ # cannot use split on possibly invalid UTF-8, but we can use partition()
193
+ preamble, _, rest = body.partition("--"+boundary)
194
+ raise("preamble before boundary preamble="+preamble.inspect()) unless preamble.empty?
195
+ raise("no multipart ending found, expected --\\r\\n at end") unless rest.end_with?(boundary+"--\r\n")
196
+ until rest == "--\r\n"
197
+ part, _, rest = rest.partition("--"+boundary)
198
+ spec, _, value = part.chomp.partition("\r\n\r\n")
199
+ spec =~ /; name="([^"]+)"/m
200
+ name = $1
201
+ spec =~ /; filename="([^"]+)"/m
202
+ filename = $1
203
+ spec =~ /Content-Type: ([^"]+)/m
204
+ mimetype = $1
205
+ if mimetype.nil?
206
+ @fields[name] = value.force_encoding("UTF-8")
207
+ else
208
+ @fields[name] = {"filename" => filename, "mimetype" => mimetype, "contents" => value}
209
+ end
210
+ end
211
+ @fields
212
+ end
213
+
214
+ # return the request as a raw HTTP string
215
+ def to_s()
216
+ @headers["Content-Length"] = @body.bytes.size() unless @body.empty?
217
+ msg = ["#{@method} #{@url} HTTP/1.1"]
218
+ @headers.keys.each { |k|
219
+ msg << "#{k}: #{@headers[k]}"
220
+ }
221
+ msg.join("\r\n") + "\r\n\r\n" + @body
222
+ end
223
223
 
224
224
  end
225
225
  end
@@ -4,54 +4,54 @@ module Diode
4
4
 
5
5
  class Response
6
6
 
7
- STATUS = {
8
- 200 => "OK",
9
- 400 => "Bad Request",
10
- 403 => "Forbidden",
11
- 404 => "Not Found",
12
- 405 => "Method Not Allowed"
13
- }
14
-
15
- DEFAULT_HEADERS = {
16
- "Content-Type" => "application/json",
17
- "Date" => Time.now.rfc2822(),
18
- "Cache-Control" => "no-store",
19
- "Server" => "Diode/1.0",
20
- "Connection" => "Keep-Alive"
21
- }
22
-
23
- # returns the html for a few status codes
24
- def self.standard(code)
25
- raise("code #{code} is not supported") unless STATUS.key?(code)
26
- message = STATUS[code]
27
- body = "<html><head><title>#{message}</title></head><body><h1>#{code} - #{message}</h1></body></html>\n"
28
- h = DEFAULT_HEADERS.merge({"Content-Type" => "text/html"})
29
- new(code, body, h)
30
- end
31
-
32
- def self.xml(code, body="", headers={})
33
- resp = Response.new(code, body, headers)
34
- resp.headers["Content-Type"] = "application/xml"
35
- return resp
36
- end
37
-
38
- attr_accessor(:code, :body, :headers)
39
-
40
- def initialize(code, body="", headers={})
41
- @code = code.to_i()
42
- @body = body
43
- @headers = DEFAULT_HEADERS.merge(headers)
44
- end
45
-
46
- # return the response as a raw HTTP string
47
- def to_s()
48
- @headers["Content-Length"] = @body.bytes.size() unless @body.empty?
49
- msg = ["HTTP/1.1 #{@code} #{STATUS[@code]}"]
50
- @headers.keys.each { |k|
51
- msg << "#{k}: #{@headers[k]}"
52
- }
53
- msg.join("\r\n") + "\r\n\r\n" + @body
54
- end
7
+ STATUS = {
8
+ 200 => "OK",
9
+ 400 => "Bad Request",
10
+ 403 => "Forbidden",
11
+ 404 => "Not Found",
12
+ 405 => "Method Not Allowed"
13
+ }
14
+
15
+ DEFAULT_HEADERS = {
16
+ "Content-Type" => "application/json",
17
+ "Date" => Time.now.httpdate(),
18
+ "Cache-Control" => "no-store",
19
+ "Server" => "Diode/1.0",
20
+ "Connection" => "Keep-Alive"
21
+ }
22
+
23
+ # returns the html for a few status codes
24
+ def self.standard(code)
25
+ raise("code #{code} is not supported") unless STATUS.key?(code)
26
+ message = STATUS[code]
27
+ body = "<html><head><title>#{message}</title></head><body><h1>#{code} - #{message}</h1></body></html>\n"
28
+ h = DEFAULT_HEADERS.merge({"Content-Type" => "text/html"})
29
+ new(code, body, h)
30
+ end
31
+
32
+ def self.xml(code, body="", headers={})
33
+ resp = Response.new(code, body, headers)
34
+ resp.headers["Content-Type"] = "application/xml"
35
+ return resp
36
+ end
37
+
38
+ attr_accessor(:code, :body, :headers)
39
+
40
+ def initialize(code, body="", headers={})
41
+ @code = code.to_i()
42
+ @body = body
43
+ @headers = DEFAULT_HEADERS.merge(headers)
44
+ end
45
+
46
+ # return the response as a raw HTTP string
47
+ def to_s()
48
+ @headers["Content-Length"] = @body.bytes.size() unless @body.empty?
49
+ msg = ["HTTP/1.1 #{@code} #{STATUS[@code]}"]
50
+ @headers.keys.each { |k|
51
+ msg << "#{k}: #{@headers[k]}"
52
+ }
53
+ msg.join("\r\n") + "\r\n\r\n" + @body
54
+ end
55
55
 
56
56
  end
57
57
  end
data/lib/diode/server.rb CHANGED
@@ -1,113 +1,114 @@
1
- require 'async/io'
1
+ require 'async/scheduler'
2
2
  require 'time'
3
3
  require 'json'
4
- require 'diode/request'
5
- require 'diode/response'
6
- require 'diode/static'
4
+ require_relative 'request'
5
+ require_relative 'response'
6
+ require_relative 'static'
7
+
8
+ Fiber.set_scheduler(Async::Scheduler.new())
7
9
 
8
10
  module Diode
9
11
 
10
12
  class Server
11
13
 
12
- attr_accessor(:env, :filters)
14
+ attr_accessor(:env, :filters)
13
15
 
14
- def initialize(port, routing=[], env={})
15
- @port = port.to_i()
16
- @routing = routing
17
- @env = env
18
- @endpoint = Async::IO::Endpoint.tcp("127.0.0.1", @port)
19
- @filters = [self] # list of filters that requests pass through before dispatch to a servlet
20
- end
16
+ def initialize(port, routing=[], env={})
17
+ @port = port.to_i()
18
+ @routing = routing
19
+ @env = env
20
+ @filters = [self] # list of filters that requests pass through before dispatch to a servlet
21
+ end
21
22
 
22
- def start()
23
- validate_routing()
24
- Async do |server|
25
- Signal.trap("INT") {
26
- server.stop()
27
- }
28
- @endpoint.accept do |client|
29
- rawRequest = read_request(client)
30
- begin
31
- request = Diode::Request.new(rawRequest)
32
- request.remote = client.io.remote_address # decorate with request source address
33
- request.env = @env.dup() # copy environment into request
34
- request.filters = @filters.dup
35
- response = (request.filters.shift).serve(request)
36
- rescue Diode::SecurityError, Diode::RequestError => e
37
- response = Diode::Response.standard(e.code)
38
- end
39
- complete(client, response)
40
- client.close_write()
41
- end
42
- end
43
- rescue
44
- # ignore errors such as bad file decriptor on shutdown
45
- end
23
+ def start()
24
+ validate_routing()
25
+ Fiber.schedule {
26
+ server = TCPServer.new("127.0.0.1", @port)
27
+ Signal.trap("INT") { exit(0) }
28
+ loop do
29
+ client = server.accept()
30
+ Fiber.schedule(client) { |task, client|
31
+ rawRequest = read_request(client)
32
+ begin
33
+ request = Diode::Request.new(rawRequest)
34
+ request.remote = client.remote_address.ip_address # decorate with request source address
35
+ request.env = @env.dup() # copy environment into request
36
+ request.filters = @filters.dup
37
+ response = (request.filters.shift).serve(request)
38
+ rescue Diode::SecurityError, Diode::RequestError => e
39
+ response = Diode::Response.standard(e.code)
40
+ end
41
+ complete(client, response)
42
+ client.close_write()
43
+ }
44
+ end
45
+ }
46
+ end
46
47
 
47
- # check routing table is sane
48
- def validate_routing()
49
- newRouting = []
50
- @routing.each { |pattern, klass, *args|
51
- raise("invalid pattern='#{pattern}' in routing table") unless pattern.is_a?(Regexp)
52
- begin
53
- servletKlass = klass.split("::").inject(Object) { |o,c| o.const_get(c) }
54
- newRouting << [pattern, servletKlass, args]
55
- rescue NameError
56
- raise("unrecognised class #{klass} found in routing table")
57
- end
58
- }
59
- @routing = newRouting # optimised so we can instantiate the servlet quickly
60
- end
48
+ # check routing table is sane
49
+ def validate_routing()
50
+ newRouting = []
51
+ @routing.each { |pattern, klass, *args|
52
+ raise("invalid pattern='#{pattern}' in routing table") unless pattern.is_a?(Regexp)
53
+ begin
54
+ servletKlass = klass.split("::").inject(Object) { |o,c| o.const_get(c) }
55
+ newRouting << [pattern, servletKlass, args]
56
+ rescue NameError
57
+ raise("unrecognised class #{klass} found in routing table")
58
+ end
59
+ }
60
+ @routing = newRouting # optimised so we can instantiate the servlet quickly
61
+ end
61
62
 
62
- def read_request(client)
63
- message = client.recv(2048000)
64
- unless message.empty?
65
- unless message.index("Content-Length: ").nil? # handle large messages such as file uploads
66
- start = message.index("Content-Length: ")
67
- stop = message.index("\r\n", start)
68
- len = message[start..stop].chomp.sub("Content-Length: ","").to_i
69
- bodyStart = message.index("\r\n\r\n") + 3 # up to end of 4 bytes
70
- byteStart = message[0..bodyStart].bytes.size
71
- remaining = len - (message.bytes.size - byteStart) # we have already read some of the content
72
- while remaining > 0
73
- chunk = client.recv(2048000)
74
- message = message + chunk
75
- remaining = remaining - chunk.bytes.size
76
- end
77
- end
78
- end
79
- message
80
- end
63
+ def read_request(client)
64
+ message = client.recv(2048000)
65
+ unless message.empty?
66
+ unless message.index("Content-Length: ").nil? # handle large messages such as file uploads
67
+ start = message.index("Content-Length: ")
68
+ stop = message.index("\r\n", start)
69
+ len = message[start..stop].chomp.sub("Content-Length: ","").to_i
70
+ bodyStart = message.index("\r\n\r\n") + 3 # up to end of 4 bytes
71
+ byteStart = message[0..bodyStart].bytes.size
72
+ remaining = len - (message.bytes.size - byteStart) # we have already read some of the content
73
+ while remaining > 0
74
+ chunk = client.recv(2048000)
75
+ message = message + chunk
76
+ remaining = remaining - chunk.bytes.size
77
+ end
78
+ end
79
+ end
80
+ message
81
+ end
81
82
 
82
- # handle a new request connection
83
- def serve(request) # keep signature consistent with filters and servlets
84
- pattern, klass, args = @routing.find{ |pattern, klass, args|
85
- not pattern.match(request.path).nil?
86
- }
87
- raise(Diode::RequestError.new(404)) if klass.nil?
88
- servlet = klass.new(*args)
89
- request.pattern = pattern # provide the mount pattern to the request, useful for Diode::Static
90
- response = servlet.serve(request)
91
- end
83
+ # handle a new request connection
84
+ def serve(request) # keep signature consistent with filters and servlets
85
+ pattern, klass, args = @routing.find{ |pattern, klass, args|
86
+ not pattern.match(request.path).nil?
87
+ }
88
+ raise(Diode::RequestError.new(404)) if klass.nil?
89
+ servlet = klass.new(*args)
90
+ request.pattern = pattern # provide the mount pattern to the request, useful for Diode::Static
91
+ response = servlet.serve(request)
92
+ end
92
93
 
93
- def complete(conn, response) # send the response, make sure its all sent
94
- begin
95
- http = response.to_s
96
- total = http.bytes.size()
97
- sent = 0
98
- while sent < total
99
- msgsize = conn.send(http.byteslice(sent..-1), 0)
100
- sent = sent + msgsize
101
- end
102
- rescue Errno::EPIPE # ignore, we're finished anyway
103
- end
104
- end
94
+ def complete(conn, response) # send the response, make sure its all sent
95
+ begin
96
+ http = response.to_s
97
+ total = http.bytes.size()
98
+ sent = 0
99
+ while sent < total
100
+ msgsize = conn.send(http.byteslice(sent..-1), 0)
101
+ sent = sent + msgsize
102
+ end
103
+ rescue Errno::EPIPE # ignore, we're finished anyway
104
+ end
105
+ end
105
106
 
106
- def url_encode(s)
107
- s.b.gsub(/([^ a-zA-Z0-9_.-]+)/) { |m|
108
- '%' + m.unpack('H2' * m.bytesize).join('%').upcase
109
- }.tr(' ', '+').force_encoding(Encoding::UTF_8) # url-encoded message
110
- end
107
+ def url_encode(s)
108
+ s.b.gsub(/([^ a-zA-Z0-9_.-]+)/) { |m|
109
+ '%' + m.unpack('H2' * m.bytesize).join('%').upcase
110
+ }.tr(' ', '+').force_encoding(Encoding::UTF_8) # url-encoded message
111
+ end
111
112
 
112
113
  end
113
114
  end
metadata CHANGED
@@ -1,43 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: diode
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.2
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kjell Koda
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-26 00:00:00.000000000 Z
11
+ date: 2024-04-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: '2'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: '2'
27
27
  - !ruby/object:Gem::Dependency
28
- name: async-io
28
+ name: async
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ">="
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
33
+ version: '2'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ">="
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
40
+ version: '2'
41
41
  description: "Diode helps you to build REST application servers by providing a container
42
42
  for your servlets.\n About as simple as rack but more powerful, and nowhere near
43
43
  as complex as passenger, Diode has only four classes\n that make it easy to get
@@ -64,7 +64,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
- version: '0'
67
+ version: 3.2.0
68
68
  required_rubygems_version: !ruby/object:Gem::Requirement
69
69
  requirements:
70
70
  - - ">="