em-midori 0.0.5 → 0.0.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f69b25e1336f4b2fd3a632c84f3560430a71c310
4
- data.tar.gz: 94f873cd0e20c3fc3583a989f64d92fa4b625729
3
+ metadata.gz: 49747119b2524c9b0f81ca22a1c9f27010b880de
4
+ data.tar.gz: d32a3a67fd807cdeb7c49d5b7a90aacd24f24f43
5
5
  SHA512:
6
- metadata.gz: 0362c3f0361363d6a716c7bd9e679f7ea13ff0c75a3dbc693f1f26c54ab70fcccfc982d1761a44d21591fcd2d0cd9aa3988dd8b0829ea3c1f39e2f307462e17c
7
- data.tar.gz: 1a3ea21bf031fac25875876314ca31b650271a6b061d5fa22337c74218272e655ebec3c91db15124ccae47394372937c424756d50bfaf31fb93f66065ea71542
6
+ metadata.gz: 51732ede10120a05f89e960826aadffb5342efec233eaaff238a9b82c887c905e09cae0357b8c6802f05db0f77e011c709cd0fa2b3bcce366e8aee92297a524e
7
+ data.tar.gz: c7e2497cdeed26883d3a5dff6d1195a77c27926ad46e01fd3a0bb556abcdd8bb0f5fd14a7191cbb7c7f881450c23894ff7f5e389db6551a7751fecbfc0976ce3
data/.gitignore CHANGED
@@ -49,3 +49,5 @@ build-iPhoneSimulator/
49
49
 
50
50
  # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
51
51
  .rvmrc
52
+
53
+ /.vscode/
Binary file
data/.travis.yml CHANGED
@@ -9,16 +9,16 @@ rvm:
9
9
 
10
10
  jdk:
11
11
  - openjdk7
12
- - oraclejdk7
12
+ - oraclejdk8
13
13
 
14
14
  matrix:
15
15
  exclude:
16
16
  - rvm: 2.2.5
17
- jdk: oraclejdk7
17
+ jdk: openjdk7
18
18
  - rvm: 2.3.1
19
- jdk: oraclejdk7
19
+ jdk: openjdk7
20
20
  - rvm: rbx-3.20
21
- jdk: oraclejdk7
21
+ jdk: openjdk7
22
22
  allow_failures:
23
23
  - rvm: jruby-head
24
24
  - rvm: rbx-3.20
data/lib/em-midori.rb CHANGED
@@ -1,10 +1,14 @@
1
+ require 'digest/sha1'
1
2
  require 'em-midori/version'
3
+ require 'em-midori/debug'
4
+ require 'em-midori/const'
2
5
  require 'em-midori/define_class'
6
+ require 'em-midori/error'
3
7
  require 'em-midori/clean_room'
4
8
  require 'em-midori/em_midori'
5
9
  require 'em-midori/request'
6
10
  require 'em-midori/response'
7
11
  require 'em-midori/api'
12
+ require 'em-midori/route'
8
13
  require 'em-midori/server'
9
-
10
- # Midori.run(Midori::API, '0.0.0.0', 8080)
14
+ require 'em-midori/websocket'
data/lib/em-midori/api.rb CHANGED
@@ -155,6 +155,13 @@ class Midori::API
155
155
  # end
156
156
  def eventsource(path, &block) end
157
157
 
158
+ # Implementation of route DSL
159
+ # === Attributes
160
+ # * +method+ [+String+] - HTTP method
161
+ # * +path+ [+String+, +Regexp+] - path definition
162
+ # * +block+ [+Proc+] - process to run when route matched
163
+ # === Returns
164
+ # nil
158
165
  def add_route(method, path, block)
159
166
  @route = Array.new if @route.nil?
160
167
  if path.class == String
@@ -165,24 +172,33 @@ class Midori::API
165
172
  nil
166
173
  end
167
174
 
168
- def receive(request)
169
- @route.each do |route|
170
- matched = match(route.method, route.path, request)
171
- if matched
172
- # puts "route matched: #{route.method} #{route.path}"
173
- clean_room = CleanRoom.new
174
- begin
175
- result = lambda {clean_room.instance_exec(*matched, &route.function)}.call
175
+ # Process after receive data from client
176
+ # === Attributes
177
+ # * +request+ [+StringIO+] - Http Raw Request
178
+ # === Returns
179
+ # [+Midori::Response+] - Http response
180
+ def receive(request, connection=nil)
181
+ @route.each do |route|
182
+ matched = match(route.method, route.path, request.method, request.path)
183
+ if matched
184
+ clean_room = CleanRoom.new(request)
185
+ if request.websocket?
186
+ # Send 101 Switching Protocol
187
+ connection.send_data Midori::Response.new(101, {
188
+ 'Upgrade' => 'websocket',
189
+ 'Connection' => 'Upgrade',
190
+ 'Sec-WebSocket-Accept' => Digest::SHA1.base64digest(request.header['Sec-WebSocket-Key'] +'258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
191
+ }, '')
192
+ result = lambda {clean_room.instance_exec(connection.websocket, *matched, &route.function)}.call
193
+ return Midori::Response.new
194
+ else
195
+ result = lambda {clean_room.instance_exec(*matched, &route.function)}.call
196
+ end
176
197
  clean_room.body = result if result.class == String
177
198
  return clean_room.response
178
- rescue => e
179
- puts e
180
- return Midori::Response.new(500, {}, 'Internal Server Error')
181
199
  end
182
200
  end
183
- end
184
- # 404
185
- Midori::Response.new(404, {}, '404 Not Found')
201
+ raise Midori::Error::NotFound
186
202
  end
187
203
 
188
204
  # Match route with given definition
@@ -196,10 +212,9 @@ class Midori::API
196
212
  # else returns an array of parameter string matched
197
213
  # === Examples
198
214
  # match('GET', /^\/user\/(.*?)\/order\/(.*?)$/, '/user/foo/order/bar') # => ['foo', 'bar']
199
- def match(method, path, request)
200
- request = request.lines.first.split
201
- if request[0] == method
202
- result = request[1].match(path)
215
+ def match(method, path, request_method, request_path)
216
+ if request_method == method
217
+ result = request_path.match(path)
203
218
  return result.to_a[1..-1] if result
204
219
  false
205
220
  else
@@ -233,14 +248,4 @@ class Midori::API
233
248
  add_route(method.upcase, args[0], block) #args[0]: path
234
249
  end
235
250
  end
236
-
237
251
  end
238
-
239
- class Midori::Route
240
- attr_accessor :method, :path, :function
241
- def initialize(method, path, function)
242
- @method = method
243
- @path = path
244
- @function = function
245
- end
246
- end
@@ -1,9 +1,10 @@
1
1
  class CleanRoom
2
- attr_accessor :code, :header, :body
3
- def initialize
2
+ attr_accessor :code, :header, :body, :request
3
+ def initialize(request)
4
4
  @status = 200
5
5
  @header = {}
6
6
  @body = ''
7
+ @request = request
7
8
  end
8
9
 
9
10
  def response
@@ -0,0 +1,48 @@
1
+ module Midori::Const
2
+ STATUS_CODE = {
3
+ 100 => '100 Continue',
4
+ 101 => '101 Switching Protocols',
5
+ 200 => '200 OK',
6
+ 201 => '201 Created',
7
+ 202 => '202 Accepted',
8
+ 203 => '203 Non-Authoritative Information',
9
+ 204 => '204 No Content',
10
+ 205 => '205 Reset Content',
11
+ 206 => '206 Partial Content',
12
+ 300 => '300 Multiple Choices',
13
+ 301 => '301 Moved Permanently',
14
+ 304 => '304 Not Modified',
15
+ 305 => '305 Use Proxy',
16
+ 307 => '307 Temporary Redirect',
17
+ 400 => '400 Bad Request',
18
+ 401 => '401 Unauthorized',
19
+ 402 => '402 Payment Required',
20
+ 403 => '403 Forbidden',
21
+ 404 => '404 Not Found',
22
+ 405 => '405 Method Not Allowed',
23
+ 406 => '406 Not Acceptable',
24
+ 407 => '407 Proxy Authentication Required',
25
+ 408 => '408 Request Time-out',
26
+ 409 => '409 Conflict',
27
+ 410 => '410 Gone',
28
+ 411 => '411 Length Required',
29
+ 412 => '412 Precondition Failed',
30
+ 413 => '413 Request Entity Too Large',
31
+ 414 => '414 Request-URI Too Large',
32
+ 415 => '415 Unsupported Media Type',
33
+ 416 => '416 Requested range not satisfiable',
34
+ 417 => '417 Expectation Failed',
35
+ 500 => '500 Internal Server Error',
36
+ 501 => '501 Not Implemented',
37
+ 502 => '502 Bad Gateway',
38
+ 503 => '503 Service Unavailable',
39
+ 504 => '504 Gateway Time-out',
40
+ 505 => '505 HTTP Version not supported'
41
+ }
42
+ STATUS_CODE.default= '500 Internal Server Error'
43
+ STATUS_CODE.freeze
44
+
45
+ DEFAULT_HEADER = {
46
+ "Server" => "Midori/#{Midori::VERSION}"
47
+ }
48
+ end
@@ -0,0 +1,29 @@
1
+ class String
2
+ def colorize(color_code)
3
+ "\e[#{color_code}m#{self}\e[0m"
4
+ end
5
+
6
+ def red
7
+ colorize(31)
8
+ end
9
+
10
+ def green
11
+ colorize(32)
12
+ end
13
+
14
+ def yellow
15
+ colorize(33)
16
+ end
17
+
18
+ def blue
19
+ colorize(34)
20
+ end
21
+
22
+ def pink
23
+ colorize(35)
24
+ end
25
+
26
+ def light_blue
27
+ colorize(36)
28
+ end
29
+ end
@@ -6,4 +6,11 @@ module Kernel #:nodoc:
6
6
  Object.const_get(name).class_eval(&Proc.new) if block_given?
7
7
  Object.const_get(name)
8
8
  end
9
+
10
+ def define_error(*args)
11
+ args.each do |arg|
12
+ class_name = arg.to_s.split('_').map {|word| word[0] = word[0].upcase; word}.join
13
+ define_class(class_name, StandardError)
14
+ end
15
+ end
9
16
  end
@@ -1,24 +1,23 @@
1
1
  require 'eventmachine'
2
2
 
3
3
  module Midori
4
- def self.run(api=(Midori::API), ip=nil, port=nil)
4
+ def self.run(api=Midori::API, ip=nil, port=nil)
5
5
  ip ||= '127.0.0.1'
6
6
  port ||= 8081
7
7
  EventMachine.run do
8
- puts "Midori #{Midori::VERSION} is now running on #{ip}:#{port}"
9
- Midori::Server.api = api
10
- @midori_server = EventMachine.start_server ip, port, Midori::Server
8
+ puts "Midori #{Midori::VERSION} is now running on #{ip}:#{port}".blue
9
+ @midori_server = EventMachine.start_server ip, port, Midori::Server, api
11
10
  end
12
11
  end
13
12
 
14
13
  def self.stop
15
14
  if @midori_server.nil?
16
- puts 'Midori Server has NOT been started'
15
+ puts 'Midori Server has NOT been started'.red
17
16
  return false
18
17
  else
19
18
  EventMachine.stop_server(@midori_server)
20
19
  @midori_server = nil
21
- puts 'Goodbye Midori'
20
+ puts 'Goodbye Midori'.blue
22
21
  return true
23
22
  end
24
23
  end
@@ -0,0 +1,7 @@
1
+ module Midori::Error
2
+ class NotFound < StandardError; end
3
+ class ContinuousFrame < StandardError; end
4
+ class OpCodeError < StandardError; end
5
+ class NotMasked < StandardError; end
6
+ class FrameEnd < StandardError; end
7
+ end
@@ -1,3 +1,64 @@
1
+ require 'stringio'
2
+
1
3
  class Midori::Request
4
+ attr_accessor :ip, :port,
5
+ :protocol, :method, :path, :query_string,
6
+ :header, :body
7
+
8
+ def initialize
9
+ @parsed = false
10
+ @is_websocket = false
11
+ @is_eventsource = false
12
+ end
13
+
14
+ # Init an request with StringIO data
15
+ # === Attributes
16
+ # * +data+ [+StringIO+] - Request data
17
+ def parse(data)
18
+ @header = Hash.new
19
+
20
+ # Parse request
21
+ line = data.gets.split
22
+ @protocol = line[2]
23
+ @method = line[0]
24
+ @query_string = line[1].match(/\?(.*?)$/)
25
+ unless @query_string.nil?
26
+ @query_string = @query_string[1]
27
+ end
28
+ @path = line[1].gsub(/\?(.*?)$/, '')
29
+
30
+ # Parse header
31
+ while (line = data.gets) != "\r\n"
32
+ line = line.split
33
+ @header[line[0][0..-2]] = line[1..-1].join(' ')
34
+ end
35
+
36
+ # Deal with WebSocket
37
+ if @header['Upgrade'] == 'websocket' && @header['Connection'] == 'Upgrade'
38
+ @method = 'WEBSOCKET'
39
+ @is_websocket = true
40
+ end
41
+
42
+ # Deal with EventSource
43
+ if @header['Accept'] == 'text/event-stream'
44
+ @method = 'EVENTSOURCE'
45
+ @is_eventsource = true
46
+ end
47
+
48
+ # Parse body
49
+ @body = data.read
50
+ @parsed = true
51
+ end
52
+
53
+ def parsed?
54
+ @parsed
55
+ end
56
+
57
+ def websocket?
58
+ @is_websocket
59
+ end
2
60
 
61
+ def eventsource?
62
+ @is_eventsource
63
+ end
3
64
  end
@@ -1,56 +1,19 @@
1
1
  class Midori::Response
2
- STATUS_CODE = {
3
- 100 => '100 Continue',
4
- 101 => '101 Switching Protocols',
5
- 200 => '200 OK',
6
- 201 => '201 Created',
7
- 202 => '202 Accepted',
8
- 203 => '203 Non-Authoritative Information',
9
- 204 => '204 No Content',
10
- 205 => '205 Reset Content',
11
- 206 => '206 Partial Content',
12
- 300 => '300 Multiple Choices',
13
- 301 => '301 Moved Permanently',
14
- 304 => '304 Not Modified',
15
- 305 => '305 Use Proxy',
16
- 307 => '307 Temporary Redirect',
17
- 400 => '400 Bad Request',
18
- 401 => '401 Unauthorized',
19
- 402 => '402 Payment Required',
20
- 403 => '403 Forbidden',
21
- 404 => '404 Not Found',
22
- 405 => '405 Method Not Allowed',
23
- 406 => '406 Not Acceptable',
24
- 407 => '407 Proxy Authentication Required',
25
- 408 => '408 Request Time-out',
26
- 409 => '409 Conflict',
27
- 410 => '410 Gone',
28
- 411 => '411 Length Required',
29
- 412 => '412 Precondition Failed',
30
- 413 => '413 Request Entity Too Large',
31
- 414 => '414 Request-URI Too Large',
32
- 415 => '415 Unsupported Media Type',
33
- 416 => '416 Requested range not satisfiable',
34
- 417 => '417 Expectation Failed',
35
- 500 => '500 Internal Server Error',
36
- 501 => '501 Not Implemented',
37
- 502 => '502 Bad Gateway',
38
- 503 => '503 Service Unavailable',
39
- 504 => '504 Gateway Time-out',
40
- 505 => '505 HTTP Version not supported'
41
- }
42
- STATUS_CODE.default= '500 Internal Server Error'
43
- STATUS_CODE.freeze
2
+ attr_accessor :status, :header, :body
44
3
 
45
- attr_accessor :status_code, :header, :body
46
-
47
- def initialize(code=200, header={}, body='')
48
- @status_code = STATUS_CODE[code]
4
+ def initialize(code=200, header= Midori::Const::DEFAULT_HEADER, body='')
5
+ @status = Midori::Const::STATUS_CODE[code]
49
6
  @header = header
50
7
  @body = body
51
8
  end
52
9
 
10
+ def generate_header
11
+ @header.map do |key, value|
12
+ "#{key}: #{value}\r\n"
13
+ end.join
14
+ end
15
+
53
16
  def to_s
54
- "HTTP/1.1 #{@status_code}\r\nServer: Midori/#{Midori::VERSION}\r\n\r\n#{@body}"
17
+ "HTTP/1.1 #{@status}\r\n#{generate_header}\r\n#{@body}"
55
18
  end
56
19
  end
@@ -0,0 +1,8 @@
1
+ class Midori::Route
2
+ attr_accessor :method, :path, :function
3
+ def initialize(method, path, function)
4
+ @method = method
5
+ @path = path
6
+ @function = function
7
+ end
8
+ end
@@ -1,15 +1,78 @@
1
+ require 'stringio'
2
+
1
3
  module Midori::Server
2
- class << self
3
- attr_accessor :api
4
+ attr_accessor :request, :api, :websocket
5
+
6
+ def initialize(api)
7
+ @api = api
8
+ @request = Midori::Request.new
9
+ @websocket = Midori::WebSocket.new(self)
4
10
  end
5
11
 
6
12
  def receive_data(data)
13
+ start_time = Time.now
14
+ data = StringIO.new(data)
7
15
  port, ip = Socket.unpack_sockaddr_in(get_peername)
8
- response = Midori::Server.api.receive(data)
9
- puts "from #{ip}:#{port} comes a message:"
10
- puts data # Debug
11
- puts response # Debug
12
- send_data response
13
- close_connection_after_writing
16
+ @request.ip = ip
17
+ @request.port = port
18
+ if @request.parsed?
19
+ websocket_request(data)
20
+ else
21
+ receive_new_request(data)
22
+ end
23
+ $stdout << "#{@request.ip} - - [#{Time.now.inspect}] \"#{@request.method} #{@request.path} #{@request.protocol}\" #{@response.status} #{(Time.now.to_f - start_time.to_f).round(5)}\n".green
14
24
  end
25
+
26
+ def receive_new_request(data)
27
+ begin
28
+ @request.parse(data)
29
+ @response = @api.receive(request, self)
30
+ rescue Midori::Error::NotFound => _e
31
+ @response = Midori::Response.new(404, {}, '404 Not Found')
32
+ rescue => e
33
+ @response = Midori::Response.new(500, {}, 'Internal Server Error')
34
+ puts e.inspect.yellow
35
+ end
36
+ unless (@request.websocket? || @request.eventsource?)
37
+ send_data @response
38
+ close_connection_after_writing
39
+ end
40
+ if @request.websocket? && !@websocket.events[:open].nil?
41
+ call_event(:open)
42
+ end
43
+ end
44
+
45
+ def websocket_request(data)
46
+ begin
47
+ @websocket.decode(data)
48
+ case @websocket.opcode
49
+ when 0x1, 0x2
50
+ call_event(:message, @websocket.msg)
51
+ when 0x9
52
+ call_event(:ping)
53
+ @websocket.pong
54
+ when 0xA
55
+ call_event(:pong)
56
+ else
57
+ # Unknown Control Frame
58
+ raise Midori::Error::FrameEnd
59
+ end
60
+ rescue Midori::Error::FrameEnd => _e
61
+ unless @websocket.events[:close].nil?
62
+ call_event(:close)
63
+ end
64
+ send_data "\b" # Opcode 0x8
65
+ close_connection_after_writing
66
+ rescue => e
67
+ puts e.inspect.yellow
68
+ @response = Midori::Response.new(400, {}, 'Bad Request')
69
+ send_data @response
70
+ close_connection_after_writing
71
+ end
72
+ end
73
+
74
+ def call_event(event, args=[])
75
+ lambda {@websocket.instance_exec(*args, &@websocket.events[event])}.call unless @websocket.events[event].nil?
76
+ end
77
+
15
78
  end
@@ -1,3 +1,3 @@
1
1
  module Midori
2
- VERSION = '0.0.5'.freeze
2
+ VERSION = '0.0.6'.freeze
3
3
  end
@@ -0,0 +1,63 @@
1
+ class Midori::WebSocket
2
+ attr_accessor :msg, :opcode, :events, :connection
3
+
4
+ def initialize(connection)
5
+ @events = Hash.new
6
+ @connection = connection
7
+ end
8
+
9
+ def decode(data)
10
+ # Fin and Opcode
11
+ byte_tmp = data.getbyte
12
+ fin = byte_tmp & 0b10000000
13
+ @opcode = byte_tmp & 0b00001111
14
+ raise Midori::Error::ContinuousFrame unless fin
15
+ raise Midori::Error::OpCodeError unless [0x1, 0x2, 0x8, 0x9, 0xA].include?opcode
16
+ raise Midori::Error::FrameEnd if @opcode == 0x8 # Close Frame
17
+ return if @opcode == 0x9 || @opcode == 0xA # Ping Pong
18
+ decode_mask(data)
19
+ end
20
+
21
+ def decode_mask(data)
22
+ # Mask
23
+ byte_tmp = data.getbyte
24
+ is_masked = byte_tmp & 0b10000000
25
+ raise Midori::Error::NotMasked unless is_masked
26
+ # Payload
27
+ payload = byte_tmp & 0b01111111
28
+ mask = 4.times.map {data.getbyte}
29
+ # Message
30
+ masked_msg = payload.times.map {data.getbyte}
31
+ @msg = masked_msg.each_with_index.map {|byte, i| byte ^ mask[i % 4]}
32
+ @msg = @msg.pack('C*').force_encoding('utf-8') if @opcode == 0x1
33
+ end
34
+
35
+ def on(event, &block) # open, message, close, ping, pong
36
+ @events[event] = block
37
+ end
38
+
39
+ def send(msg)
40
+ output = Array.new
41
+ if msg.is_a?String
42
+ output << 0b10000001 << msg.size << msg
43
+ elsif msg.is_a?Array
44
+ output << 0b10000010 << msg.size
45
+ output.concat msg
46
+ else
47
+ raise Midori::Error::OpCodeError
48
+ end
49
+ @connection.send_data(output.pack("CCA#{msg.size}"))
50
+ end
51
+
52
+ def ping
53
+ @connection.send_data "\t"
54
+ end
55
+
56
+ def pong
57
+ @connection.send_data "\n"
58
+ end
59
+
60
+ def close
61
+ Midori::Error::FrameEnd
62
+ end
63
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: em-midori
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - HeckPsi Lab
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-09-25 00:00:00.000000000 Z
11
+ date: 2016-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: eventmachine
@@ -72,6 +72,20 @@ dependencies:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
74
  version: '3.0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: faye-websocket
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '0.10'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '0.10'
75
89
  description: An EventMachine Based Web Framework on Ruby
76
90
  email: business@heckpsi.com
77
91
  executables: []
@@ -82,6 +96,7 @@ files:
82
96
  - ".gitignore"
83
97
  - ".resources/midori_tokiwa.gif"
84
98
  - ".resources/sapphire_kawashima.gif"
99
+ - ".resources/slogan.png"
85
100
  - ".rspec"
86
101
  - ".rubocop.yml"
87
102
  - ".travis.yml"
@@ -89,12 +104,17 @@ files:
89
104
  - lib/em-midori.rb
90
105
  - lib/em-midori/api.rb
91
106
  - lib/em-midori/clean_room.rb
107
+ - lib/em-midori/const.rb
108
+ - lib/em-midori/debug.rb
92
109
  - lib/em-midori/define_class.rb
93
110
  - lib/em-midori/em_midori.rb
111
+ - lib/em-midori/error.rb
94
112
  - lib/em-midori/request.rb
95
113
  - lib/em-midori/response.rb
114
+ - lib/em-midori/route.rb
96
115
  - lib/em-midori/server.rb
97
116
  - lib/em-midori/version.rb
117
+ - lib/em-midori/websocket.rb
98
118
  homepage: https://github.com/heckpsi-lab/em-midori
99
119
  licenses:
100
120
  - MIT
@@ -120,3 +140,4 @@ signing_key:
120
140
  specification_version: 4
121
141
  summary: An EventMachine Based Web Framework on Ruby
122
142
  test_files: []
143
+ has_rdoc: