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 +4 -4
- data/lib/diode/request.rb +210 -210
- data/lib/diode/response.rb +48 -48
- data/lib/diode/server.rb +97 -96
- metadata +12 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b8b71e839db24b48761cbd168c49dcd69ba03c9eba9033738f3b6998561dff8f
|
4
|
+
data.tar.gz: d9fd0af9f07501b19c534fd9209973ad07002b9aef0ec5c00d3163f71f3e3e93
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
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
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
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
|
data/lib/diode/response.rb
CHANGED
@@ -4,54 +4,54 @@ module Diode
|
|
4
4
|
|
5
5
|
class Response
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
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/
|
1
|
+
require 'async/scheduler'
|
2
2
|
require 'time'
|
3
3
|
require 'json'
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
-
|
14
|
+
attr_accessor(:env, :filters)
|
13
15
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
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.
|
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:
|
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: '
|
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: '
|
26
|
+
version: '2'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name: async
|
28
|
+
name: async
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
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: '
|
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:
|
67
|
+
version: 3.2.0
|
68
68
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
69
|
requirements:
|
70
70
|
- - ">="
|