em-midori 0.0.5 → 0.0.6

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
  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: