websocket-rack-noodles 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,68 @@
1
+ module Rack
2
+ module WebSocket
3
+ module Handler
4
+ class Base
5
+
6
+ autoload :Connection, "#{ROOT_PATH}/websocket/handler/base/connection"
7
+
8
+ def on_open
9
+ set_env_instance_variable
10
+ @parent.on_open(@env)
11
+ end # Fired when a client is connected.
12
+
13
+ def on_message(msg)
14
+ set_env_instance_variable
15
+ @parent.on_message(@env, msg)
16
+ end # Fired when a message from a client is received.
17
+
18
+ def on_close
19
+ set_env_instance_variable
20
+ @parent.on_close(@env)
21
+ end # Fired when a client is disconnected.
22
+
23
+ def on_error(error)
24
+ set_env_instance_variable
25
+ @parent.on_error(@env, error)
26
+ end # Fired when error occurs.
27
+
28
+ # Set application as parent and forward options
29
+ def initialize(parent, options = {})
30
+ @parent = parent
31
+ @options = options[:backend] || {}
32
+ end
33
+
34
+ def set_env_instance_variable
35
+ @parent.instance_variable_set("@env", @env)
36
+ end
37
+
38
+ # Implemented in subclass
39
+ def call(env)
40
+ raise 'Not implemented'
41
+ end
42
+
43
+ # Implemented in subclass
44
+ def send_data(data)
45
+ raise 'Not implemented'
46
+ end
47
+
48
+ # Implemented in subclass
49
+ def close_websocket
50
+ raise 'Not implemented'
51
+ end
52
+
53
+ protected
54
+
55
+ # Standard async response
56
+ def async_response
57
+ [-1, {}, []]
58
+ end
59
+
60
+ # Standard 400 response
61
+ def failure_response
62
+ [ 400, {'Content-Type' => 'text/plain'}, [ 'Bad request' ] ]
63
+ end
64
+
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,15 @@
1
+ module Rack
2
+ module WebSocket
3
+ module Handler
4
+ class Stub < Base
5
+
6
+ # Always close socket
7
+ def call(env)
8
+ raise 'Unknown handler!'
9
+ close_websocket
10
+ end
11
+
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,58 @@
1
+ require 'thin'
2
+
3
+ module Rack
4
+ module WebSocket
5
+ module Handler
6
+ class Thin < Base
7
+
8
+ # Build request from Rack env
9
+ def call(env)
10
+ @env = env
11
+ socket = env['async.connection']
12
+ request = request_from_env(env)
13
+ @connection = Connection.new(self, socket, :debug => @options[:debug])
14
+ @connection.dispatch(request) ? async_response : failure_response
15
+ end
16
+
17
+ # Forward send_data to server
18
+ def send_data(data)
19
+ if @connection
20
+ @connection.send data
21
+ else
22
+ raise WebSocketError, "WebSocket not opened"
23
+ end
24
+ end
25
+
26
+ # Forward close_websocket to server
27
+ def close_websocket
28
+ if @connection
29
+ @connection.close_websocket
30
+ else
31
+ raise WebSocketError, "WebSocket not opened"
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ # Parse Rack env to em-websocket-compatible format
38
+ # this probably should be moved to Base in future
39
+ def request_from_env(env)
40
+ request = {}
41
+ request['path'] = env['REQUEST_URI'].to_s
42
+ request['method'] = env['REQUEST_METHOD']
43
+ request['query'] = env['QUERY_STRING'].to_s
44
+ request['Body'] = env['rack.input'].read
45
+
46
+ env.each do |key, value|
47
+ if key.match(/HTTP_(.+)/)
48
+ request[$1.downcase.gsub('_','-')] ||= value
49
+ end
50
+ end
51
+
52
+ request
53
+ end
54
+
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,21 @@
1
+ module Rack
2
+ module WebSocket
3
+ module Handler
4
+
5
+ autoload :Base, "#{ROOT_PATH}/websocket/handler/base"
6
+ autoload :Stub, "#{ROOT_PATH}/websocket/handler/stub"
7
+ autoload :Thin, "#{ROOT_PATH}/websocket/handler/thin"
8
+
9
+ # Detect current server using software Rack string
10
+ def self.detect(env)
11
+ server_software = env['SERVER_SOFTWARE']
12
+ if server_software.match(/\Athin /i)
13
+ Thin
14
+ else
15
+ Stub
16
+ end
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ module Rack
2
+ module WebSocket
3
+ VERSION = "0.4.0"
4
+ end
5
+ end
@@ -0,0 +1,14 @@
1
+ require 'rack'
2
+ require 'em-websocket'
3
+
4
+ module Rack
5
+ module WebSocket
6
+ ROOT_PATH = ::File.expand_path(::File.dirname(__FILE__))
7
+
8
+ autoload :Application, "#{ROOT_PATH}/websocket/application"
9
+ autoload :Extensions, "#{ROOT_PATH}/websocket/extensions"
10
+ autoload :Handler, "#{ROOT_PATH}/websocket/handler"
11
+ end
12
+ end
13
+
14
+ Rack::WebSocket::Extensions.apply!
@@ -0,0 +1,18 @@
1
+ require 'rubygems'
2
+ require 'rspec'
3
+
4
+ require 'rack/websocket'
5
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
6
+
7
+ RSpec.configure do |config|
8
+ config.mock_with :mocha
9
+ end
10
+
11
+ class TestApp < Rack::WebSocket::Application
12
+ end
13
+
14
+ TEST_PORT = 8081
15
+
16
+ def new_server_connection
17
+ TCPSocket.new('localhost', TEST_PORT)
18
+ end
@@ -0,0 +1,44 @@
1
+ require 'timeout'
2
+ shared_examples_for 'all drafts' do
3
+ it "should accept incoming connection" do
4
+ conn = new_server_connection
5
+ conn.write(handshake_request)
6
+ timeout(1) { conn.read(handshake_response.length).should eql(handshake_response) }
7
+ end
8
+ it "should call 'on_open' on new connection" do
9
+ TestApp.any_instance.expects(:on_open)
10
+ conn = new_server_connection
11
+ conn.write(handshake_request)
12
+ end
13
+ it "should call 'on_open' on new connection with proper env" do
14
+ TestApp.any_instance.expects(:on_open).once.with { |env| env.class == Hash && !env.keys.empty? }
15
+ conn = new_server_connection
16
+ conn.write(handshake_request)
17
+ end
18
+ it "should call 'on_close' on connection close" do
19
+ TestApp.any_instance.expects(:on_close)
20
+ conn = new_server_connection
21
+ conn.write(handshake_request)
22
+ conn.close
23
+ end
24
+ it "should call 'on_close' on connection close with proper env" do
25
+ TestApp.any_instance.expects(:on_close).once.with { |env| env.class == Hash && !env.keys.empty? }
26
+ conn = new_server_connection
27
+ conn.write(handshake_request)
28
+ conn.close
29
+ end
30
+ it "should call 'on_message' on connection sending data" do
31
+ TestApp.any_instance.expects(:on_message)
32
+ conn = new_server_connection
33
+ conn.write(handshake_request)
34
+ timeout(1) { conn.read(handshake_response.length) }
35
+ conn.write(message)
36
+ end
37
+ it "should call 'on_message' on connection sending data with proper env and message" do
38
+ TestApp.any_instance.expects(:on_message).once.with { |env, message| env.class == Hash && !env.keys.empty? && message == 'Hello' }
39
+ conn = new_server_connection
40
+ conn.write(handshake_request)
41
+ timeout(1) { conn.read(handshake_response.length) }
42
+ conn.write(message)
43
+ end
44
+ end
@@ -0,0 +1,80 @@
1
+ shared_examples_for 'all handlers' do
2
+ it "should return flash policy file" do
3
+ conn = new_server_connection
4
+ conn.write(flash_policy_request)
5
+ conn.read(flash_policy_response.length).should eql(flash_policy_response)
6
+ end
7
+
8
+ context 'for draft75' do
9
+ let(:handshake_request) { spec75_handshake_request }
10
+ let(:handshake_response) { spec75_handshake_response }
11
+ let(:message) { spec75_message }
12
+
13
+ it_should_behave_like 'all drafts'
14
+ end
15
+
16
+ # Also draft00
17
+ context 'for draft76' do
18
+ let(:handshake_request) { spec76_handshake_request }
19
+ let(:handshake_response) { spec76_handshake_response }
20
+ let(:message) { spec76_message }
21
+
22
+ it_should_behave_like 'all drafts'
23
+ end
24
+
25
+ # Drafts 01, 02 and 03 are pretty the same so one test for all
26
+ context 'for draft03' do
27
+ let(:handshake_request) { spec03_handshake_request }
28
+ let(:handshake_response) { spec03_handshake_response }
29
+ let(:message) { spec03_message }
30
+
31
+ it_should_behave_like 'all drafts'
32
+ end
33
+
34
+ context 'for draft05' do
35
+ let(:handshake_request) { spec05_handshake_request }
36
+ let(:handshake_response) { spec05_handshake_response }
37
+ let(:message) { spec05_message }
38
+
39
+ it_should_behave_like 'all drafts'
40
+ end
41
+
42
+ context 'for draft06' do
43
+ let(:handshake_request) { spec06_handshake_request }
44
+ let(:handshake_response) { spec06_handshake_response }
45
+ let(:message) { spec06_message }
46
+
47
+ it_should_behave_like 'all drafts'
48
+ end
49
+
50
+ context 'for draft07' do
51
+ let(:handshake_request) { spec07_handshake_request }
52
+ let(:handshake_response) { spec07_handshake_response }
53
+ let(:message) { spec07_unmasked_message }
54
+ let(:masked_message) { spec07_masked_message }
55
+
56
+ it_should_behave_like 'all drafts'
57
+ it_should_behave_like 'draft with masked messages'
58
+ end
59
+
60
+ context 'for draft08' do
61
+ let(:handshake_request) { spec08_handshake_request }
62
+ let(:handshake_response) { spec08_handshake_response }
63
+ let(:message) { spec08_unmasked_message }
64
+ let(:masked_message) { spec08_masked_message }
65
+
66
+ it_should_behave_like 'all drafts'
67
+ it_should_behave_like 'draft with masked messages'
68
+ end
69
+
70
+ # Drafts 09, 10, 11, 12 and 13 are pretty the same so one test for all
71
+ context 'for draft13' do
72
+ let(:handshake_request) { spec13_handshake_request }
73
+ let(:handshake_response) { spec13_handshake_response }
74
+ let(:message) { spec13_unmasked_message }
75
+ let(:masked_message) { spec13_masked_message }
76
+
77
+ it_should_behave_like 'all drafts'
78
+ it_should_behave_like 'draft with masked messages'
79
+ end
80
+ end
@@ -0,0 +1,9 @@
1
+ shared_examples_for 'draft with masked messages' do
2
+ it "should call 'on_message' on connection sending masked data with proper env and message" do
3
+ TestApp.any_instance.expects(:on_message).once.with { |env, message| env.class == Hash && !env.keys.empty? && message == 'Hello' }
4
+ conn = new_server_connection
5
+ conn.write(handshake_request)
6
+ timeout(1) { conn.read(handshake_response.length) }
7
+ conn.write(masked_message)
8
+ end
9
+ end
@@ -0,0 +1,249 @@
1
+ def flash_policy_request
2
+ "<policy-file-request/>\000"
3
+ end
4
+
5
+ def flash_policy_response
6
+ '<?xml version="1.0"?><cross-domain-policy><allow-access-from domain="*" to-ports="*"/></cross-domain-policy>'
7
+ end
8
+
9
+ def spec75_handshake_request
10
+ <<-EOF
11
+ GET /demo HTTP/1.1\r
12
+ Upgrade: WebSocket\r
13
+ Connection: Upgrade\r
14
+ Host: localhost:#{TEST_PORT}\r
15
+ Origin: http://localhost:#{TEST_PORT}\r
16
+ \r
17
+ EOF
18
+ end
19
+
20
+ def spec75_handshake_response
21
+ <<-EOF
22
+ HTTP/1.1 101 Web Socket Protocol Handshake\r
23
+ Upgrade: WebSocket\r
24
+ Connection: Upgrade\r
25
+ WebSocket-Origin: http://localhost:#{TEST_PORT}\r
26
+ WebSocket-Location: ws://localhost:#{TEST_PORT}/demo\r
27
+ \r
28
+ EOF
29
+ end
30
+
31
+ def spec75_message
32
+ "\x00Hello\xff"
33
+ end
34
+
35
+ def spec76_handshake_request
36
+ request = <<-EOF
37
+ GET /demo HTTP/1.1\r
38
+ Host: localhost:#{TEST_PORT}\r
39
+ Connection: Upgrade\r
40
+ Sec-WebSocket-Key2: 12998 5 Y3 1 .P00\r
41
+ Sec-WebSocket-Protocol: sample\r
42
+ Upgrade: WebSocket\r
43
+ Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5\r
44
+ Origin: http://localhost:#{TEST_PORT}\r
45
+ \r
46
+ ^n:ds[4U
47
+ EOF
48
+ request.rstrip
49
+ end
50
+
51
+ def spec76_handshake_response
52
+ response = <<-EOF
53
+ HTTP/1.1 101 WebSocket Protocol Handshake\r
54
+ Upgrade: WebSocket\r
55
+ Connection: Upgrade\r
56
+ Sec-WebSocket-Location: ws://localhost:#{TEST_PORT}/demo\r
57
+ Sec-WebSocket-Origin: http://localhost:#{TEST_PORT}\r
58
+ Sec-WebSocket-Protocol: sample\r
59
+ \r
60
+ 8jKS'y:G*Co,Wxa-
61
+ EOF
62
+ response.rstrip
63
+ end
64
+
65
+ def spec76_message
66
+ "\x00Hello\xff"
67
+ end
68
+
69
+ def spec03_handshake_request
70
+ request = <<-EOF
71
+ GET /demo HTTP/1.1\r
72
+ Host: localhost:#{TEST_PORT}\r
73
+ Connection: Upgrade\r
74
+ Sec-WebSocket-Key2: 12998 5 Y3 1 .P00\r
75
+ Sec-WebSocket-Protocol: sample\r
76
+ Upgrade: WebSocket\r
77
+ Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5\r
78
+ Origin: http://localhost:#{TEST_PORT}\r
79
+ Sec-WebSocket-Draft: 3\r
80
+ \r
81
+ ^n:ds[4U
82
+ EOF
83
+ request.rstrip
84
+ end
85
+
86
+ def spec03_handshake_response
87
+ response = <<-EOF
88
+ HTTP/1.1 101 WebSocket Protocol Handshake\r
89
+ Upgrade: WebSocket\r
90
+ Connection: Upgrade\r
91
+ Sec-WebSocket-Location: ws://localhost:#{TEST_PORT}/demo\r
92
+ Sec-WebSocket-Origin: http://localhost:#{TEST_PORT}\r
93
+ Sec-WebSocket-Protocol: sample\r
94
+ \r
95
+ 8jKS'y:G*Co,Wxa-
96
+ EOF
97
+ response.rstrip
98
+ end
99
+
100
+ def spec03_message
101
+ "\x04\x05Hello"
102
+ end
103
+
104
+ def spec05_handshake_request
105
+ <<-EOF
106
+ GET /chat HTTP/1.1\r
107
+ Host: localhost:#{TEST_PORT}\r
108
+ Upgrade: websocket\r
109
+ Connection: Upgrade\r
110
+ Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
111
+ Sec-WebSocket-Origin: http://localhost:#{TEST_PORT}\r
112
+ Sec-WebSocket-Protocol: chat, superchat\r
113
+ Sec-WebSocket-Version: 5\r
114
+ \r
115
+ EOF
116
+ end
117
+
118
+ def spec05_handshake_response
119
+ <<-EOF
120
+ HTTP/1.1 101 Switching Protocols\r
121
+ Upgrade: websocket\r
122
+ Connection: Upgrade\r
123
+ Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r
124
+ EOF
125
+ end
126
+
127
+ def spec05_message
128
+ "\x00\x00\x01\x00\x84\x05Ielln"
129
+ end
130
+
131
+ def spec06_handshake_request
132
+ <<-EOF
133
+ GET /chat HTTP/1.1\r
134
+ Host: localhost:#{TEST_PORT}\r
135
+ Upgrade: websocket\r
136
+ Connection: Upgrade\r
137
+ Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
138
+ Sec-WebSocket-Origin: http://localhost:#{TEST_PORT}\r
139
+ Sec-WebSocket-Protocol: chat, superchat\r
140
+ Sec-WebSocket-Version: 6\r
141
+ \r
142
+ EOF
143
+ end
144
+
145
+ def spec06_handshake_response
146
+ <<-EOF
147
+ HTTP/1.1 101 Switching Protocols\r
148
+ Upgrade: websocket\r
149
+ Connection: Upgrade\r
150
+ Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r
151
+ EOF
152
+ end
153
+
154
+ def spec06_message
155
+ "\x00\x00\x01\x00\x84\x05Ielln"
156
+ end
157
+
158
+ def spec07_handshake_request
159
+ <<-EOF
160
+ GET /chat HTTP/1.1\r
161
+ Host: localhost:#{TEST_PORT}\r
162
+ Upgrade: websocket\r
163
+ Connection: Upgrade\r
164
+ Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
165
+ Sec-WebSocket-Origin: http://localhost:#{TEST_PORT}\r
166
+ Sec-WebSocket-Protocol: chat, superchat\r
167
+ Sec-WebSocket-Version: 7\r
168
+ \r
169
+ EOF
170
+ end
171
+
172
+ def spec07_handshake_response
173
+ <<-EOF
174
+ HTTP/1.1 101 Switching Protocols\r
175
+ Upgrade: websocket\r
176
+ Connection: Upgrade\r
177
+ Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r
178
+ EOF
179
+ end
180
+
181
+ def spec07_unmasked_message
182
+ "\x81\x05\x48\x65\x6c\x6c\x6f"
183
+ end
184
+
185
+ def spec07_masked_message
186
+ "\x81\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58"
187
+ end
188
+
189
+ def spec08_handshake_request
190
+ <<-EOF
191
+ GET /chat HTTP/1.1\r
192
+ Host: localhost:#{TEST_PORT}\r
193
+ Upgrade: websocket\r
194
+ Connection: Upgrade\r
195
+ Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
196
+ Sec-WebSocket-Origin: http://localhost:#{TEST_PORT}\r
197
+ Sec-WebSocket-Protocol: chat, superchat\r
198
+ Sec-WebSocket-Version: 8\r
199
+ \r
200
+ EOF
201
+ end
202
+
203
+ def spec08_handshake_response
204
+ <<-EOF
205
+ HTTP/1.1 101 Switching Protocols\r
206
+ Upgrade: websocket\r
207
+ Connection: Upgrade\r
208
+ Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r
209
+ EOF
210
+ end
211
+
212
+ def spec08_unmasked_message
213
+ "\x81\x05\x48\x65\x6c\x6c\x6f"
214
+ end
215
+
216
+ def spec08_masked_message
217
+ "\x81\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58"
218
+ end
219
+
220
+ def spec13_handshake_request
221
+ <<-EOF
222
+ GET /chat HTTP/1.1\r
223
+ Host: localhost:#{TEST_PORT}\r
224
+ Upgrade: websocket\r
225
+ Connection: Upgrade\r
226
+ Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
227
+ Origin: http://localhost:#{TEST_PORT}\r
228
+ Sec-WebSocket-Protocol: chat, superchat\r
229
+ Sec-WebSocket-Version: 13\r
230
+ \r
231
+ EOF
232
+ end
233
+
234
+ def spec13_handshake_response
235
+ <<-EOF
236
+ HTTP/1.1 101 Switching Protocols\r
237
+ Upgrade: websocket\r
238
+ Connection: Upgrade\r
239
+ Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r
240
+ EOF
241
+ end
242
+
243
+ def spec13_unmasked_message
244
+ "\x81\x05\x48\x65\x6c\x6c\x6f"
245
+ end
246
+
247
+ def spec13_masked_message
248
+ "\x81\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58"
249
+ end
data/spec/thin_spec.rb ADDED
@@ -0,0 +1,48 @@
1
+ require 'thin'
2
+ require 'spec_helper'
3
+
4
+ describe 'Thin handler' do
5
+ let(:app) { TestApp.new }
6
+
7
+ before(:all) { silent_thin }
8
+ before { start_thin_server(app) }
9
+ after { stop_thin_server }
10
+
11
+ it "should include extensions" do
12
+ ::Thin::Connection.include?(::Rack::WebSocket::Extensions::Common).should be_true
13
+ ::Thin::Connection.include?(::Rack::WebSocket::Extensions::Thin::Connection).should be_true
14
+ end
15
+
16
+ it_should_behave_like 'all handlers'
17
+ end
18
+
19
+ def start_thin_server(app, options = {})
20
+ @server = Thin::Server.new('0.0.0.0', TEST_PORT, options, app)
21
+ @server.ssl = options[:ssl]
22
+ # @server.threaded = options[:threaded]
23
+ # @server.timeout = 3
24
+
25
+ @thread = Thread.new { @server.start }
26
+ sleep 1 until @server.running?
27
+ end
28
+
29
+ def stop_thin_server
30
+ sleep 0.1
31
+ @server.stop!
32
+ sleep 0.1
33
+ @thread.kill
34
+ sleep 0.1
35
+ raise "Reactor still running, wtf?" if EventMachine.reactor_running?
36
+ end
37
+
38
+ def silent_thin
39
+ ::Thin::Logging.silent = true
40
+ if EM::VERSION < "1.0.0"
41
+ begin
42
+ old_verbose, $VERBOSE = $VERBOSE, nil
43
+ ::Thin::Server.const_set 'DEFAULT_TIMEOUT', 0
44
+ ensure
45
+ $VERBOSE = old_verbose
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "rack/websocket/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "websocket-rack-noodles"
7
+ s.version = Rack::WebSocket::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Bernard Potocki"]
10
+ s.email = ["bernard.potocki@imanel.org"]
11
+ s.homepage = "http://github.com/DamirSvrtan/websocket-rack"
12
+ s.summary = %q{Rack-based WebSocket server for Noodles Web Framework}
13
+ s.description = %q{Rack-based WebSocket server for Noodles Web Framework. An fork from http://github.com/imanel/websocket-rack}
14
+
15
+ s.add_dependency 'rack'
16
+ s.add_dependency 'em-websocket', '~> 0.3.8'
17
+ s.add_dependency 'eventmachine', '~> 1.0.0'
18
+ s.add_dependency 'thin' # Temporary until we support more servers
19
+
20
+ s.files = `git ls-files`.split("\n")
21
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
22
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
23
+ s.require_paths = ["lib"]
24
+ end