rocket-core 0.0.2

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.
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
22
+ *.rdb
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Araneo Ltd.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
File without changes
data/Rakefile ADDED
@@ -0,0 +1,43 @@
1
+ # -*- ruby -*-
2
+ $:.unshift(File.expand_path('../lib', __FILE__))
3
+ $:.unshift(File.expand_path('../../rocket/lib', __FILE__))
4
+ require 'rspec/core/rake_task'
5
+ require 'rake/rdoctask'
6
+ require 'rocket/version'
7
+
8
+ RSpec::Core::RakeTask.new(:spec) do |t|
9
+ t.pattern = 'spec/**/*_spec.rb'
10
+ t.rspec_opts = %q[-I../rocket/lib -Ilib -c -b]
11
+ end
12
+
13
+ RSpec::Core::RakeTask.new(:rcov) do |t|
14
+ t.rcov = true
15
+ t.rspec_opts = %q[-I../rocket/lib -Ilib -c -b]
16
+ t.rcov_opts = %q[-I../rocket/lib -Ilib -T -x "spec"]
17
+ end
18
+
19
+ Rake::RDocTask.new do |rdoc|
20
+ rdoc.title = "Rocket core #{Rocket.version}"
21
+ rdoc.rdoc_dir = 'rdoc'
22
+ rdoc.rdoc_files.include('README*')
23
+ rdoc.rdoc_files.include('lib/**/*.rb')
24
+ end
25
+
26
+ desc "Build current version as a rubygem"
27
+ task :build do
28
+ sh "gem build rocket-core.gemspec"
29
+ sh "mkdir -p pkg"
30
+ sh "mv rocket-core-*.gem pkg/"
31
+ end
32
+
33
+ desc "Relase current version to rubygems.org"
34
+ task :release => :build do
35
+ sh "gem push pkg/rocket-core-#{Rocket.version}.gem"
36
+ end
37
+
38
+ desc "Perform installation via rubygems"
39
+ task :install => :build do
40
+ sh "gem install pkg/rocket-core-#{Rocket.version}.gem"
41
+ end
42
+
43
+ task :default => :spec
@@ -0,0 +1,8 @@
1
+ module Rocket
2
+
3
+ autoload :Helpers, 'rocket/helpers'
4
+ autoload :Client, 'rocket/client'
5
+ autoload :Channel, 'rocket/channel'
6
+ autoload :WebSocket, 'rocket/websocket'
7
+
8
+ end # Rocket
@@ -0,0 +1,21 @@
1
+ require 'json'
2
+
3
+ module Rocket
4
+ class Channel
5
+
6
+ attr_reader :client
7
+ attr_reader :channel
8
+
9
+ def initialize(client, channel)
10
+ @client = client
11
+ @channel = channel
12
+ end
13
+
14
+ def trigger(event, data)
15
+ client.connection do |socket|
16
+ socket.send({ :event => event, :channel => channel, :data => data }.to_json)
17
+ end
18
+ end
19
+
20
+ end # Channel
21
+ end # Rocket
@@ -0,0 +1,31 @@
1
+ require 'json'
2
+
3
+ module Rocket
4
+ # This Rocket client allows for triggering events via Rocket server.
5
+ #
6
+ # rocket = Rocket::Client.new("ws://host.com:9772", "my-app", "my53cr37k3y")
7
+ # rocket['my-channel'].trigger('event', { :hello => :world })
8
+ #
9
+ class Client
10
+
11
+ attr_reader :url
12
+ attr_reader :app_id
13
+ attr_reader :secret
14
+
15
+ def initialize(url, app_id, secret)
16
+ @url = File.join(url, 'app', app_id) + '?secret=' + secret
17
+ @app_id = app_id
18
+ end
19
+
20
+ def connection(&block)
21
+ socket = WebSocket.new(url)
22
+ yield(socket)
23
+ socket.close if socket && !socket.tcp_socket.closed?
24
+ end
25
+
26
+ def [](channel)
27
+ Channel.new(self, channel)
28
+ end
29
+
30
+ end # Client
31
+ end # Rocket
@@ -0,0 +1,23 @@
1
+ module Rocket
2
+ module Helpers
3
+
4
+ # Hash given, returns it version with symbolized keys.
5
+ #
6
+ # p symbolize_keys("hello" => "world", ["Array here"] => "yup")
7
+ # p symbolize_keys(:one => 1, "two" => 2)
8
+ #
9
+ # produces:
10
+ #
11
+ # {:hello => "world", ["Array here"] => "yup"}
12
+ # {:one => 1, :two => 2}
13
+ #
14
+ def symbolize_keys(hash)
15
+ return hash unless hash.is_a?(Hash)
16
+ hash.inject({}) do |options, (key, value)|
17
+ options[(key.to_sym if key.respond_to?(:to_sym)) || key] = value
18
+ options
19
+ end
20
+ end
21
+
22
+ end # Helpers
23
+ end # Rocket
@@ -0,0 +1,225 @@
1
+ # Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
2
+ # Lincense: New BSD Lincense
3
+ # Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol
4
+
5
+ require "socket"
6
+ require "uri"
7
+ require "digest/md5"
8
+ require "openssl"
9
+
10
+ module Rocket
11
+ class WebSocket
12
+
13
+ class Error < RuntimeError; end
14
+
15
+ class << self
16
+ attr_accessor :debug
17
+ end
18
+
19
+ attr_reader :header
20
+ attr_reader :path
21
+
22
+ def initialize(arg, params = {})
23
+ uri = arg.is_a?(String) ? URI.parse(arg) : arg
24
+
25
+ if uri.scheme == "ws"
26
+ default_port = 80
27
+ elsif uri.scheme = "wss"
28
+ default_port = 443
29
+ else
30
+ raise(WebSocket::Error, "unsupported scheme: #{uri.scheme}")
31
+ end
32
+
33
+ @path = (uri.path.empty? ? "/" : uri.path) + (uri.query ? "?" + uri.query : "")
34
+ host = uri.host + (uri.port == default_port ? "" : ":#{uri.port}")
35
+ origin = params[:origin] || "http://#{uri.host}"
36
+ key1 = generate_key()
37
+ key2 = generate_key()
38
+ key3 = generate_key3()
39
+
40
+ socket = TCPSocket.new(uri.host, uri.port || default_port)
41
+
42
+ if uri.scheme == "ws"
43
+ @socket = socket
44
+ else
45
+ @socket = ssl_handshake(socket)
46
+ end
47
+
48
+ write(
49
+ "GET #{@path} HTTP/1.1\r\n" +
50
+ "Upgrade: WebSocket\r\n" +
51
+ "Connection: Upgrade\r\n" +
52
+ "Host: #{host}\r\n" +
53
+ "Origin: #{origin}\r\n" +
54
+ "Sec-WebSocket-Key1: #{key1}\r\n" +
55
+ "Sec-WebSocket-Key2: #{key2}\r\n" +
56
+ "\r\n" +
57
+ "#{key3}")
58
+ flush()
59
+
60
+ line = gets().chomp()
61
+ raise(WebSocket::Error, "bad response: #{line}") if !(line =~ /\AHTTP\/1.1 101 /n)
62
+ read_header()
63
+ if @header["Sec-WebSocket-Origin"] != origin
64
+ raise(WebSocket::Error,
65
+ "origin doesn't match: '#{@header["WebSocket-Origin"]}' != '#{origin}'")
66
+ end
67
+ reply_digest = read(16)
68
+ expected_digest = security_digest(key1, key2, key3)
69
+ if reply_digest != expected_digest
70
+ raise(WebSocket::Error,
71
+ "security digest doesn't match: %p != %p" % [reply_digest, expected_digest])
72
+ end
73
+ @handshaked = true
74
+ @closing_started = false
75
+ end
76
+
77
+ def send(data)
78
+ if !@handshaked
79
+ raise(WebSocket::Error, "call WebSocket\#handshake first")
80
+ end
81
+ data = force_encoding(data.dup(), "ASCII-8BIT")
82
+ write("\x00#{data}\xff")
83
+ flush()
84
+ end
85
+
86
+ def receive()
87
+ if !@handshaked
88
+ raise(WebSocket::Error, "call WebSocket\#handshake first")
89
+ end
90
+ packet = gets("\xff")
91
+ return nil if !packet
92
+ if packet =~ /\A\x00(.*)\xff\z/nm
93
+ return force_encoding($1, "UTF-8")
94
+ elsif packet == "\xff" && read(1) == "\x00" # closing
95
+ if @server
96
+ @socket.close()
97
+ else
98
+ close()
99
+ end
100
+ return nil
101
+ else
102
+ raise(WebSocket::Error, "input must be either '\\x00...\\xff' or '\\xff\\x00'")
103
+ end
104
+ end
105
+
106
+ def tcp_socket
107
+ return @socket
108
+ end
109
+
110
+ def host
111
+ return @header["Host"]
112
+ end
113
+
114
+ def origin
115
+ return @header["Origin"]
116
+ end
117
+
118
+ # Does closing handshake.
119
+ def close()
120
+ return if @closing_started
121
+ write("\xff\x00")
122
+ @socket.close() if !@server
123
+ @closing_started = true
124
+ end
125
+
126
+ def close_socket()
127
+ @socket.close()
128
+ end
129
+
130
+ private
131
+
132
+ NOISE_CHARS = ("\x21".."\x2f").to_a() + ("\x3a".."\x7e").to_a()
133
+
134
+ def read_header()
135
+ @header = {}
136
+ while line = gets()
137
+ line = line.chomp()
138
+ break if line.empty?
139
+ if !(line =~ /\A(\S+): (.*)\z/n)
140
+ raise(WebSocket::Error, "invalid request: #{line}")
141
+ end
142
+ @header[$1] = $2
143
+ end
144
+ if @header["Upgrade"] != "WebSocket"
145
+ raise(WebSocket::Error, "invalid Upgrade: " + @header["Upgrade"])
146
+ end
147
+ if @header["Connection"] != "Upgrade"
148
+ raise(WebSocket::Error, "invalid Connection: " + @header["Connection"])
149
+ end
150
+ end
151
+
152
+ def gets(rs = $/)
153
+ line = @socket.gets(rs)
154
+ $stderr.printf("recv> %p\n", line) if WebSocket.debug
155
+ return line
156
+ end
157
+
158
+ def read(num_bytes)
159
+ str = @socket.read(num_bytes)
160
+ $stderr.printf("recv> %p\n", str) if WebSocket.debug
161
+ return str
162
+ end
163
+
164
+ def write(data)
165
+ if WebSocket.debug
166
+ data.scan(/\G(.*?(\n|\z))/n) do
167
+ $stderr.printf("send> %p\n", $&) if !$&.empty?
168
+ end
169
+ end
170
+ @socket.write(data)
171
+ end
172
+
173
+ def flush()
174
+ @socket.flush()
175
+ end
176
+
177
+ def security_digest(key1, key2, key3)
178
+ bytes1 = websocket_key_to_bytes(key1)
179
+ bytes2 = websocket_key_to_bytes(key2)
180
+ return Digest::MD5.digest(bytes1 + bytes2 + key3)
181
+ end
182
+
183
+ def generate_key()
184
+ spaces = 1 + rand(12)
185
+ max = 0xffffffff / spaces
186
+ number = rand(max + 1)
187
+ key = (number * spaces).to_s()
188
+ (1 + rand(12)).times() do
189
+ char = NOISE_CHARS[rand(NOISE_CHARS.size)]
190
+ pos = rand(key.size + 1)
191
+ key[pos...pos] = char
192
+ end
193
+ spaces.times() do
194
+ pos = 1 + rand(key.size - 1)
195
+ key[pos...pos] = " "
196
+ end
197
+ return key
198
+ end
199
+
200
+ def generate_key3()
201
+ return [rand(0x100000000)].pack("N") + [rand(0x100000000)].pack("N")
202
+ end
203
+
204
+ def websocket_key_to_bytes(key)
205
+ num = key.gsub(/[^\d]/n, "").to_i() / key.scan(/ /).size
206
+ return [num].pack("N")
207
+ end
208
+
209
+ def force_encoding(str, encoding)
210
+ if str.respond_to?(:force_encoding)
211
+ return str.force_encoding(encoding)
212
+ else
213
+ return str
214
+ end
215
+ end
216
+
217
+ def ssl_handshake(socket)
218
+ ssl_context = OpenSSL::SSL::SSLContext.new()
219
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
220
+ ssl_socket.sync_close = true
221
+ ssl_socket.connect()
222
+ return ssl_socket
223
+ end
224
+ end # WebSocket
225
+ end # Rocket
@@ -0,0 +1,22 @@
1
+ # -*- ruby -*-
2
+ $:.unshift(File.expand_path('../lib', __FILE__))
3
+ $:.unshift(File.expand_path('../../rocket/lib', __FILE__))
4
+ require 'rocket/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = 'rocket-core'
8
+ s.version = Rocket.version
9
+ s.homepage = 'http://github.com/araneo/rocket'
10
+ s.email = ['chris@nu7hat.ch']
11
+ s.authors = ['Araneo Ltd.', 'Chris Kowalik']
12
+ s.summary = %q{Rocket core package.}
13
+ s.description = %q{Core utils used by various Rocket components.}
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {spec}/*`.split("\n")
16
+ s.require_paths = %w[lib]
17
+ s.extra_rdoc_files = %w[LICENSE README.md]
18
+
19
+ s.add_runtime_dependency 'json', ['>= 1.4']
20
+ s.add_development_dependency 'rspec', ["~> 2.0"]
21
+ s.add_development_dependency 'mocha', [">= 0.9"]
22
+ end
@@ -0,0 +1,21 @@
1
+ require File.expand_path("../spec_helper", __FILE__)
2
+
3
+ class HelpersPoweredClass
4
+ include Rocket::Helpers
5
+ end
6
+
7
+ describe Rocket::Helpers do
8
+ subject do
9
+ HelpersPoweredClass.new
10
+ end
11
+
12
+ describe "#symbolize_keys" do
13
+ it "should return given object when it's not a Hash" do
14
+ subject.symbolize_keys(obj = Object.new).should == obj
15
+ end
16
+
17
+ it "should symbolize given hash keys" do
18
+ subject.symbolize_keys({"hello" => "world"}).should == {:hello => "world"}
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,7 @@
1
+ require File.expand_path("../spec_helper", __FILE__)
2
+
3
+ describe Rocket do
4
+ subject do
5
+ Rocket
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+ require 'mocha'
3
+ require 'rspec'
4
+ require 'rocket'
5
+
6
+ RSpec.configure do |config|
7
+ config.mock_with :mocha
8
+ end
metadata ADDED
@@ -0,0 +1,126 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rocket-core
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 2
10
+ version: 0.0.2
11
+ platform: ruby
12
+ authors:
13
+ - Araneo Ltd.
14
+ - Chris Kowalik
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2010-11-09 00:00:00 +01:00
20
+ default_executable:
21
+ dependencies:
22
+ - !ruby/object:Gem::Dependency
23
+ name: json
24
+ prerelease: false
25
+ requirement: &id001 !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ hash: 7
31
+ segments:
32
+ - 1
33
+ - 4
34
+ version: "1.4"
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: rspec
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ hash: 3
46
+ segments:
47
+ - 2
48
+ - 0
49
+ version: "2.0"
50
+ type: :development
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ name: mocha
54
+ prerelease: false
55
+ requirement: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ hash: 25
61
+ segments:
62
+ - 0
63
+ - 9
64
+ version: "0.9"
65
+ type: :development
66
+ version_requirements: *id003
67
+ description: Core utils used by various Rocket components.
68
+ email:
69
+ - chris@nu7hat.ch
70
+ executables: []
71
+
72
+ extensions: []
73
+
74
+ extra_rdoc_files:
75
+ - LICENSE
76
+ - README.md
77
+ files:
78
+ - .gitignore
79
+ - LICENSE
80
+ - README.md
81
+ - Rakefile
82
+ - lib/rocket-core.rb
83
+ - lib/rocket/channel.rb
84
+ - lib/rocket/client.rb
85
+ - lib/rocket/helpers.rb
86
+ - lib/rocket/websocket.rb
87
+ - rocket-core.gemspec
88
+ - spec/helpers_spec.rb
89
+ - spec/rocket_spec.rb
90
+ - spec/spec_helper.rb
91
+ has_rdoc: true
92
+ homepage: http://github.com/araneo/rocket
93
+ licenses: []
94
+
95
+ post_install_message:
96
+ rdoc_options: []
97
+
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ hash: 3
106
+ segments:
107
+ - 0
108
+ version: "0"
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ none: false
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ hash: 3
115
+ segments:
116
+ - 0
117
+ version: "0"
118
+ requirements: []
119
+
120
+ rubyforge_project:
121
+ rubygems_version: 1.3.7
122
+ signing_key:
123
+ specification_version: 3
124
+ summary: Rocket core package.
125
+ test_files: []
126
+