diode 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/diode/request.rb +210 -210
- data/lib/diode/response.rb +48 -42
- 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,48 +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
|
-
|
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
|
49
55
|
|
50
56
|
end
|
51
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
|
- - ">="
|