diode 1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/diode/request.rb +226 -0
- data/lib/diode/response.rb +52 -0
- data/lib/diode/server.rb +112 -0
- data/lib/diode/static.rb +59 -0
- metadata +60 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b084a1183fbd2e06a1c0f4dec2907c3f815548747c84e92a96a7dfcf55b9b226
|
4
|
+
data.tar.gz: 67f3cb1edc3f30fb787bf1f4821c4927d6af9f2a69f49f24cf789e8124bf6b21
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c26da0160afddfbf021741d2897a935a2ce72e852ad64193325d33c5c7c136f6dc04881738dbe26c84d2808a8b4e9794638dfa24cf5b0c98a8a68372bacec5db
|
7
|
+
data.tar.gz: a80bebc9303af4eca74a1c19a2668bb7d4ff2e70db3098c332f536302ce2881b838d29e64d2578e3edfa43774f3f50f6db69507a14f484a9a7db84b06a6e8353
|
@@ -0,0 +1,226 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Diode
|
4
|
+
|
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
|
14
|
+
end
|
15
|
+
|
16
|
+
class SecurityError < StandardError
|
17
|
+
end
|
18
|
+
|
19
|
+
class Request
|
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
|
223
|
+
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module Diode
|
4
|
+
|
5
|
+
class Response
|
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
|
+
attr_accessor(:code, :body, :headers)
|
33
|
+
|
34
|
+
def initialize(code, body="", headers={})
|
35
|
+
@code = code.to_i()
|
36
|
+
@body = body
|
37
|
+
@headers = DEFAULT_HEADERS.merge(headers)
|
38
|
+
end
|
39
|
+
|
40
|
+
# return the response as a raw HTTP string
|
41
|
+
def to_s()
|
42
|
+
@headers["Content-Length"] = @body.bytes.size() unless @body.empty?
|
43
|
+
msg = ["HTTP/1.1 #{@code} #{STATUS[@code]}"]
|
44
|
+
@headers.keys.each { |k|
|
45
|
+
msg << "#{k}: #{@headers[k]}"
|
46
|
+
}
|
47
|
+
msg.join("\r\n") + "\r\n\r\n" + @body
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
data/lib/diode/server.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'async/io'
|
2
|
+
require 'time'
|
3
|
+
require 'json'
|
4
|
+
require 'diode/request'
|
5
|
+
require 'diode/response'
|
6
|
+
|
7
|
+
module Diode
|
8
|
+
|
9
|
+
class Server
|
10
|
+
|
11
|
+
attr_accessor(:env, :filters)
|
12
|
+
|
13
|
+
def initialize(port, routing=[], env={})
|
14
|
+
@port = port.to_i()
|
15
|
+
@routing = routing
|
16
|
+
@env = env
|
17
|
+
@endpoint = Async::IO::Endpoint.tcp("127.0.0.1", @port)
|
18
|
+
@filters = [self] # list of filters that requests pass through before dispatch to a servlet
|
19
|
+
end
|
20
|
+
|
21
|
+
def start()
|
22
|
+
validate_routing()
|
23
|
+
Async do |server|
|
24
|
+
Signal.trap("INT") {
|
25
|
+
server.stop()
|
26
|
+
}
|
27
|
+
@endpoint.accept do |client|
|
28
|
+
rawRequest = read_request(client)
|
29
|
+
begin
|
30
|
+
request = Diode::Request.new(rawRequest)
|
31
|
+
request.remote = client.io.remote_address # decorate with request source address
|
32
|
+
request.env = @env.dup() # copy environment into request
|
33
|
+
request.filters = @filters.dup
|
34
|
+
response = (request.filters.shift).serve(request)
|
35
|
+
rescue Diode::SecurityError, Diode::RequestError => e
|
36
|
+
response = Diode::Response.standard(e.code)
|
37
|
+
end
|
38
|
+
complete(client, response)
|
39
|
+
client.close_write()
|
40
|
+
end
|
41
|
+
end
|
42
|
+
rescue
|
43
|
+
# ignore errors such as bad file decriptor on shutdown
|
44
|
+
end
|
45
|
+
|
46
|
+
# check routing table is sane
|
47
|
+
def validate_routing()
|
48
|
+
newRouting = []
|
49
|
+
@routing.each { |pattern, klass, *args|
|
50
|
+
raise("invalid pattern='#{pattern}' in routing table") unless pattern.is_a?(Regexp)
|
51
|
+
begin
|
52
|
+
servletKlass = klass.split("::").inject(Object) { |o,c| o.const_get(c) }
|
53
|
+
newRouting << [pattern, servletKlass, args]
|
54
|
+
rescue NameError
|
55
|
+
raise("unrecognised class #{klass} found in routing table")
|
56
|
+
end
|
57
|
+
}
|
58
|
+
@routing = newRouting # optimised so we can instantiate the servlet quickly
|
59
|
+
end
|
60
|
+
|
61
|
+
def read_request(client)
|
62
|
+
message = client.recv(2048000)
|
63
|
+
unless message.empty?
|
64
|
+
unless message.index("Content-Length: ").nil? # handle large messages such as file uploads
|
65
|
+
start = message.index("Content-Length: ")
|
66
|
+
stop = message.index("\r\n", start)
|
67
|
+
len = message[start..stop].chomp.sub("Content-Length: ","").to_i
|
68
|
+
bodyStart = message.index("\r\n\r\n") + 3 # up to end of 4 bytes
|
69
|
+
byteStart = message[0..bodyStart].bytes.size
|
70
|
+
remaining = len - (message.bytes.size - byteStart) # we have already read some of the content
|
71
|
+
while remaining > 0
|
72
|
+
chunk = client.recv(2048000)
|
73
|
+
message = message + chunk
|
74
|
+
remaining = remaining - chunk.bytes.size
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
message
|
79
|
+
end
|
80
|
+
|
81
|
+
# handle a new request connection
|
82
|
+
def serve(request) # keep signature consistent with filters and servlets
|
83
|
+
pattern, klass, args = @routing.find{ |pattern, klass, args|
|
84
|
+
not pattern.match(request.path).nil?
|
85
|
+
}
|
86
|
+
raise(Diode::RequestError.new(404)) if klass.nil?
|
87
|
+
servlet = klass.new(*args)
|
88
|
+
request.pattern = pattern # provide the mount pattern to the request, useful for Diode::Static
|
89
|
+
response = servlet.serve(request)
|
90
|
+
end
|
91
|
+
|
92
|
+
def complete(conn, response) # send the response, make sure its all sent
|
93
|
+
begin
|
94
|
+
http = response.to_s
|
95
|
+
total = http.bytes.size()
|
96
|
+
sent = 0
|
97
|
+
while sent < total
|
98
|
+
msgsize = conn.send(http.byteslice(sent..-1), 0)
|
99
|
+
sent = sent + msgsize
|
100
|
+
end
|
101
|
+
rescue Errno::EPIPE # ignore, we're finished anyway
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def url_encode(s)
|
106
|
+
s.b.gsub(/([^ a-zA-Z0-9_.-]+)/) { |m|
|
107
|
+
'%' + m.unpack('H2' * m.bytesize).join('%').upcase
|
108
|
+
}.tr(' ', '+').force_encoding(Encoding::UTF_8) # url-encoded message
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
end
|
data/lib/diode/static.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
class Static
|
4
|
+
|
5
|
+
def initialize(docRoot=".")
|
6
|
+
@root = Pathname.new(Pathname.new(docRoot).cleanpath()).realpath()
|
7
|
+
raise("Cannot serve static files from path #{@root}") unless @root.directory? and @root.readable?
|
8
|
+
end
|
9
|
+
|
10
|
+
def serve(request)
|
11
|
+
return Diode::Response.new(405, "Method not allowed", {"Content-type" => "text/plain"}) unless request.method == "GET"
|
12
|
+
path = Pathname.new(request.path).cleanpath.sub(request.pattern, "") # remove the leading portion matching the mount pattern
|
13
|
+
filepath = Pathname.new(File.expand_path(path, @root))
|
14
|
+
return Diode::Response.new(404, "<html><body>File not found</body></html>", {"Content-type" => "text/html"}) unless filepath.exist?
|
15
|
+
mimetype = @@mimetypes[filepath.extname[1..-1]] || "application/octet-stream"
|
16
|
+
return Diode::Response.new(200, IO.read(filepath.to_s).b, {"Content-Type"=>mimetype, "Cache-Control" => "no-cache"})
|
17
|
+
end
|
18
|
+
|
19
|
+
@@mimetypes = {
|
20
|
+
"avi" => "video/x-msvideo",
|
21
|
+
"avif" => "image/avif",
|
22
|
+
"css" => "text/css",
|
23
|
+
"gif" => "image/gif",
|
24
|
+
"htm" => "text/html",
|
25
|
+
"html" => "text/html",
|
26
|
+
"ico" => "image/x-icon",
|
27
|
+
"jpeg" => "image/jpeg",
|
28
|
+
"jpg" => "image/jpeg",
|
29
|
+
"js" => "application/javascript",
|
30
|
+
"json" => "application/json",
|
31
|
+
"mov" => "video/quicktime",
|
32
|
+
"mp4" => "video/mp4",
|
33
|
+
"mpe" => "video/mpeg",
|
34
|
+
"mpeg" => "video/mpeg",
|
35
|
+
"mpg" => "video/mpeg",
|
36
|
+
"otf" => "font/otf",
|
37
|
+
"pdf" => "application/pdf",
|
38
|
+
"png" => "image/png",
|
39
|
+
"qt" => "video/quicktime",
|
40
|
+
"rb" => "text/plain",
|
41
|
+
"svg" => "image/svg+xml",
|
42
|
+
"tif" => "image/tiff",
|
43
|
+
"tiff" => "image/tiff",
|
44
|
+
"ttc" => "font/collection",
|
45
|
+
"ttf" => "font/ttf",
|
46
|
+
"txt" => "text/plain",
|
47
|
+
"webm" => "video/webm",
|
48
|
+
"webp" => "image/webp",
|
49
|
+
"woff" => "font/woff",
|
50
|
+
"woff2" => "font/woff2",
|
51
|
+
"xhtml" => "text/html",
|
52
|
+
"xml" => "text/xml",
|
53
|
+
}
|
54
|
+
|
55
|
+
def self.mimetypes
|
56
|
+
@@mimetypes
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: diode
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '1.0'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kjell Koda
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-07-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: async-io
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: A simple hello world gem
|
28
|
+
email: kjell@null4.net
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- lib/diode/request.rb
|
34
|
+
- lib/diode/response.rb
|
35
|
+
- lib/diode/server.rb
|
36
|
+
- lib/diode/static.rb
|
37
|
+
homepage: https://github.com/kjellhex/diode
|
38
|
+
licenses:
|
39
|
+
- MIT
|
40
|
+
metadata: {}
|
41
|
+
post_install_message:
|
42
|
+
rdoc_options: []
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
requirements: []
|
56
|
+
rubygems_version: 3.4.10
|
57
|
+
signing_key:
|
58
|
+
specification_version: 4
|
59
|
+
summary: A fast, simple, pure-ruby application server.
|
60
|
+
test_files: []
|