kosmonaut 0.2.0

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/COPYING ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2012 Krzysztof Kowalik <chris@nu7hat.ch> and folks at Cubox
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7
+ of the Software, and to permit persons to whom the Software is furnished to do
8
+ so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README ADDED
@@ -0,0 +1,94 @@
1
+ Ruby wrapper for Kosmonaut - the WebRocket client
2
+ =================================================
3
+
4
+ Kosmonaut.rb is a ruby backend client for the WebRocket.
5
+ The idea of the Kosmonaut is to keep it simple, straightforward
6
+ and easy to maintain, although to allow to build more
7
+ sophisticated libraries at top of it.
8
+
9
+ Installation
10
+ ------------
11
+ You can install it easily from rubygems:
12
+
13
+ $ gem install kosmonaut
14
+
15
+ Or using bundler, add this line to your gemfile:
16
+
17
+ gem 'kosmonaut'
18
+
19
+ Usage
20
+ -----
21
+ Kosmonaut has two components: Client and Worker. Client is
22
+ used to manage a WebRocket's vhost and broadcast messages,
23
+ for example:
24
+
25
+ c = Kosmonaut::Client.new("wr://token@127.0.0.1:8081/vhost")
26
+ c.open_channel("world")
27
+ c.broadcast("world", "hello", {:who => "Chris"})
28
+ c.broadcast("world", "bye", {:see_you_when => "Soon!"})
29
+ c.request_single_access_token(".*")
30
+
31
+ Worker is used to listen for incoming messages and handle
32
+ it in user's desired way, example:
33
+
34
+ class MyWorker < Kosmonaut::Worker
35
+ def on_message(event, data)
36
+ if event == "hello"
37
+ puts "Hello #{data[:who]}"
38
+ end
39
+ end
40
+
41
+ def on_error(err)
42
+ puts "Error encountered (code #{err.to_s})"
43
+ end
44
+
45
+ def on_exception(err)
46
+ puts "Ouch! something went wrong! Error: #{err.to_s}"
47
+ end
48
+ end
49
+
50
+ w = MyWorker.new("wr://token@127.0.0.1:8081/vhost")
51
+ w.listen
52
+
53
+ Hacking
54
+ -------
55
+ If you want to run kosmonaut.rb in development mode, first clone
56
+ the repo and install dependencies:
57
+
58
+ $ git clone https://github.com/webrocket/kosmonaut.rb.git
59
+ $ cd kosmonaut.rb
60
+ $ bundle
61
+
62
+ To run the tests you should have a `webrocket-server` instance
63
+ running with a `/test` vhost created. To create it use the
64
+ `webrocket-admin` tool:
65
+
66
+ $ webrocket-admin add_vhost /test
67
+ Reading cookie... done
68
+ Adding a vhost... done
69
+ ---
70
+ Access token for this vhost: a70d7d2c0bc5761620948b3420d18df9072ca0d1
71
+
72
+ Now get the access token and run kosmonaut's tests using
73
+ rake task:
74
+
75
+ $ VHOST_TOKEN=a70d7d2c0bc5761620948b3420d18df9072ca0d1 rake test
76
+
77
+ If you want to get debug output add a `DEBUG` environment variable
78
+ while running tests:
79
+
80
+ $ VHOST_TOKEN=... DEBUG=1 rake test
81
+
82
+ With any quirks and doubts don't hesitate to start a github issue
83
+ or email one of the maintainers.
84
+
85
+ Sponsors
86
+ --------
87
+ All the work on the project is sponsored and supported by Cubox - an
88
+ awesome dev shop from Uruguay <http://cuboxsa.com>.
89
+
90
+ Copyright
91
+ ---------
92
+ Copyright (C) 2012 Krzysztof Kowalik <chris@nu7hat.ch> and folks at Cubox
93
+
94
+ Released under the MIT license. See COPYING for details.
data/Rakefile ADDED
@@ -0,0 +1,29 @@
1
+ # -*- ruby -*-
2
+
3
+ =begin
4
+ require 'rdoc/task'
5
+ Rake::RDocTask.new do |rdoc|
6
+ rdoc.rdoc_dir = 'rdoc'
7
+ rdoc.title = "Kosmonaut - The WebRocket backend client"
8
+ rdoc.rdoc_files.include('README*')
9
+ rdoc.rdoc_files.include('lib/**/*.rb')
10
+ end
11
+ =end
12
+
13
+ require 'rake/testtask'
14
+ Rake::TestTask.new do |t|
15
+ t.libs << "test"
16
+ t.test_files = FileList['test/test*.rb']
17
+ t.verbose = true
18
+ end
19
+
20
+ task :default => :test
21
+
22
+ desc "Opens console with loaded mustang env."
23
+ task :console do
24
+ $LOAD_PATH.unshift("./lib")
25
+ require 'kosmonaut'
26
+ require 'irb'
27
+ ARGV.clear
28
+ IRB.start
29
+ end
data/kosmonaut.gemspec ADDED
@@ -0,0 +1,19 @@
1
+ # -*- ruby -*-
2
+ require 'rubygems'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "kosmonaut"
6
+ s.version = "0.2.0"
7
+ s.summary = "Ruby client for the WebRocket backend"
8
+ s.description = "The WebRocket server backend client for ruby programming language"
9
+ s.authors = ["Krzysztof Kowalik", "Cubox"]
10
+ s.email = "chris@nu7hat.ch"
11
+ s.homepage = "http://webrocket.io/"
12
+ s.license = "MIT"
13
+
14
+ s.files = Dir["{lib/**/*.rb,test/*.rb,Rakefile,README*,COPYING,*.gemspec}"]
15
+ s.test_files = Dir["test/*.rb"]
16
+ s.require_paths = ["lib"]
17
+
18
+ s.add_dependency "json", "~> 1.0"
19
+ end
data/lib/kosmonaut.rb ADDED
@@ -0,0 +1,16 @@
1
+ require 'kosmonaut/errors'
2
+ require 'kosmonaut/socket'
3
+ require 'kosmonaut/worker'
4
+ require 'kosmonaut/client'
5
+ require 'kosmonaut/version'
6
+
7
+ module Kosmonaut
8
+ extend self
9
+ attr_accessor :debug
10
+
11
+ def log(msg)
12
+ print("DEBUG: ", msg, "\n") if Kosmonaut.debug
13
+ end
14
+ end
15
+
16
+ Kosmonaut.debug = false
@@ -0,0 +1,77 @@
1
+ require 'json'
2
+ require 'uri'
3
+ require 'socket'
4
+ require 'securerandom'
5
+ require 'timeout'
6
+ require 'thread'
7
+
8
+ module Kosmonaut
9
+ class Client < Socket
10
+ include Kosmonaut
11
+
12
+ REQUEST_TIMEOUT = 5 # in seconds
13
+
14
+ def initialize(url)
15
+ super(url)
16
+ @mtx = Mutex.new
17
+ end
18
+
19
+ def broadcast(channel, event, data)
20
+ payload = ["BC", channel, event, data.to_json]
21
+ perform_request(payload)
22
+ end
23
+
24
+ def open_channel(name, type)
25
+ payload = ["OC", name, type]
26
+ perform_request(payload)
27
+ end
28
+
29
+ def close_channel(name)
30
+ payload = ["CC", name]
31
+ perform_request(payload)
32
+ end
33
+
34
+ def request_single_access_token(permission)
35
+ payload = ["AT", permission]
36
+ perform_request(payload)
37
+ end
38
+
39
+ def socket_type
40
+ "req"
41
+ end
42
+
43
+ private
44
+
45
+ def perform_request(payload)
46
+ @mtx.synchronize {
47
+ response = []
48
+ Timeout.timeout(REQUEST_TIMEOUT) {
49
+ s = connect
50
+ packet = pack(payload)
51
+ log("Client/REQ : #{packet.inspect}")
52
+ s.write(packet)
53
+ response = recv(s)
54
+ s.close
55
+ }
56
+ parse_response(response)
57
+ }
58
+ end
59
+
60
+ def parse_response(response)
61
+ cmd = response[0].to_s
62
+ log("Client/RES : #{response.join("\n").inspect}")
63
+ case cmd
64
+ when "OK"
65
+ return 0
66
+ when "ER"
67
+ errcode = response[1].to_i
68
+ error = ERRORS[errcode]
69
+ raise error.new if error
70
+ when "AT"
71
+ token = response[1].to_s
72
+ return token if token.size == 128
73
+ end
74
+ raise UnknownServerError.new
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,62 @@
1
+ module Kosmonaut
2
+ class Error < StandardError
3
+ end
4
+
5
+ class BadRequestError < Error
6
+ def initialize
7
+ super "400: Bad request"
8
+ end
9
+ end
10
+
11
+ class UnauthorizedError < Error
12
+ def initialize
13
+ super "402: Unauthorized"
14
+ end
15
+ end
16
+
17
+ class ForbiddenError < Error
18
+ def initialize
19
+ super "403: Forbidden"
20
+ end
21
+ end
22
+
23
+ class InvalidChannelNameError < Error
24
+ def initialize
25
+ super "451: Invalid channel name"
26
+ end
27
+ end
28
+
29
+ class ChannelNotFoundError < Error
30
+ def initialize
31
+ super "454: Channel not found"
32
+ end
33
+ end
34
+
35
+ class InternalError < Error
36
+ def initialize
37
+ super "597: Internal error"
38
+ end
39
+ end
40
+
41
+ class EndOfFileError < Error
42
+ def initialize
43
+ super "598: End of file"
44
+ end
45
+ end
46
+
47
+ class UnknownServerError < Error
48
+ def initialize
49
+ super "Unknown server error"
50
+ end
51
+ end
52
+
53
+ ERRORS = {
54
+ 400 => BadRequestError,
55
+ 402 => UnauthorizedError,
56
+ 403 => ForbiddenError,
57
+ 451 => InvalidChannelNameError,
58
+ 454 => ChannelNotFoundError,
59
+ 597 => InternalError,
60
+ 598 => EndOfFileError,
61
+ }
62
+ end
@@ -0,0 +1,49 @@
1
+ module Kosmonaut
2
+ class Socket
3
+ attr_reader :uri
4
+
5
+ def initialize(url)
6
+ @uri = URI.parse(url)
7
+ generate_identity
8
+ end
9
+
10
+ protected
11
+
12
+ def connect
13
+ TCPSocket.open(@uri.host, @uri.port)
14
+ end
15
+
16
+ def pack(payload=[])
17
+ payload.unshift("")
18
+ payload.unshift(@identity)
19
+ payload.join("\n") + "\n\r\n\r\n"
20
+ end
21
+
22
+ def recv(s)
23
+ data = []
24
+ possible_eom = false # possible end of message
25
+ while !s.eof?
26
+ line = s.gets
27
+ if line == "\r\n"
28
+ break if possible_eom
29
+ possible_eom = true
30
+ else
31
+ possible_eom = false
32
+ data << line.strip
33
+ end
34
+ end
35
+ data
36
+ end
37
+
38
+ private
39
+
40
+ def generate_identity
41
+ parts = []
42
+ parts << socket_type
43
+ parts << @uri.path
44
+ parts << @uri.user # secret
45
+ parts << SecureRandom.uuid
46
+ @identity = parts.join(":")
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,15 @@
1
+ module Kosmonaut
2
+ module Version
3
+ MAJOR = 0
4
+ MINOR = 2
5
+ PATCH = 0
6
+
7
+ def self.to_s
8
+ [MAJOR, MINOR, PATCH].join('.')
9
+ end
10
+ end
11
+
12
+ def self.version
13
+ Version.to_s
14
+ end
15
+ end
@@ -0,0 +1,128 @@
1
+ require 'json'
2
+ require 'thread'
3
+
4
+ module Kosmonaut
5
+ class Worker < Socket
6
+ include Kosmonaut
7
+
8
+ RECONNECT_DELAY = 1000 # in milliseconds
9
+ HEARTBEAT_INTERVAL = 2000 # in milliseconds
10
+ LIVENESS = 3
11
+
12
+ def initialize(url)
13
+ super(url)
14
+ @mtx = Mutex.new
15
+ @sock = nil
16
+ @alive = false
17
+ @reconnect_delay = RECONNECT_DELAY
18
+ @heartbeat_ivl = HEARTBEAT_INTERVAL
19
+ @heartbeat_at = 0
20
+ @liveness = 0
21
+ end
22
+
23
+ def listen
24
+ reconnect
25
+ @alive = true
26
+ while true
27
+ if !alive?
28
+ send(@sock, ["QT"])
29
+ disconnect
30
+ break
31
+ end
32
+ begin
33
+ Timeout.timeout(((@heartbeat_ivl * 2).to_f / 1000.0).to_i + 1) {
34
+ msg = recv(@sock)
35
+ @liveness = LIVENESS
36
+ log("Worker/RECV : #{msg.join("\n").inspect}")
37
+ next if msg.size < 1
38
+ cmd = msg.shift
39
+
40
+ case cmd
41
+ when "HB"
42
+ # nothing to do...
43
+ when "QT"
44
+ reconnect
45
+ next
46
+ when "TR"
47
+ message_handler(msg[0])
48
+ when "ER"
49
+ error_handler(msg.size < 1 ? 597 : msg[0])
50
+ end
51
+ }
52
+ rescue Timeout::Error, Errno::ECONNRESET
53
+ if (@liveness -= 1) == 0
54
+ sleep(@reconnect_delay.to_f / 1000.0)
55
+ reconnect
56
+ end
57
+ end
58
+ if Time.now.to_f > @heartbeat_at
59
+ send(@sock, ["HB"])
60
+ @heartbeat_at = Time.now.to_f + (@heartbeat_ivl.to_f / 1000.0)
61
+ end
62
+ end
63
+ end
64
+
65
+ def stop
66
+ @mtx.synchronize { @alive = false }
67
+ end
68
+
69
+ def alive?
70
+ @mtx.synchronize { @alive }
71
+ end
72
+
73
+ def socket_type
74
+ "dlr"
75
+ end
76
+
77
+ private
78
+
79
+ def send(s, payload)
80
+ packet = pack(payload)
81
+ @sock.write(packet)
82
+ log("Worker/SENT : #{packet.inspect}")
83
+ end
84
+
85
+ def disconnect
86
+ if @sock
87
+ @sock.close
88
+ @sock = nil
89
+ end
90
+ end
91
+
92
+ def reconnect
93
+ disconnect
94
+ @sock = connect
95
+ @sock.write(pack(["RD"]))
96
+ @liveness = LIVENESS
97
+ @heartbeat_at = Time.now.to_f + (@heartbeat_ivl.to_f / 1000.0)
98
+ end
99
+
100
+ def message_handler(data)
101
+ if respond_to?(:on_message)
102
+ payload = JSON.parse(data.to_s)
103
+ event = payload.keys.first
104
+ data = data[event]
105
+ on_message(event, data)
106
+ end
107
+ rescue => err
108
+ exception_handler(err)
109
+ end
110
+
111
+ def error_handler(errcode)
112
+ if respond_to?(:on_error)
113
+ err = ERRORS[errcode.to_i]
114
+ on_error(err ? err : UnknownServerError)
115
+ end
116
+ rescue => err
117
+ exception_handler(err)
118
+ end
119
+
120
+ def exception_handler(err)
121
+ if respond_to?(:on_exception)
122
+ on_exception(err)
123
+ else
124
+ raise err
125
+ end
126
+ end
127
+ end
128
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,5 @@
1
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __FILE__))
2
+ require "minitest/autorun"
3
+ require "kosmonaut"
4
+
5
+ Kosmonaut.debug = (ENV["DEBUG"] == "1")
@@ -0,0 +1,55 @@
1
+ require File.expand_path("../helper", __FILE__)
2
+
3
+ class TestKosmonautClient < MiniTest::Unit::TestCase
4
+ def setup
5
+ @client = Kosmonaut::Client.new("wr://#{ENV["VHOST_TOKEN"].to_s}@127.0.0.1:8081/test")
6
+ end
7
+
8
+ def test_api
9
+ # tests depends each other, so we have to run it in
10
+ # correct order...
11
+ _test_open_channel
12
+ _test_open_channel_with_invalid_name
13
+ _test_broadcast
14
+ _test_broadcast_to_not_existing_channel
15
+ _test_close_channel
16
+ _test_close_not_existing_channel
17
+ _test_request_single_access_token
18
+ end
19
+
20
+ def _test_open_channel
21
+ @client.open_channel("foo", 0)
22
+ end
23
+
24
+ def _test_open_channel_with_invalid_name
25
+ @client.open_channel("%%%", 0)
26
+ assert false
27
+ rescue Kosmonaut::InvalidChannelNameError
28
+ end
29
+
30
+ def _test_broadcast
31
+ @client.broadcast("foo", "test", {})
32
+ end
33
+
34
+ def _test_broadcast_to_not_existing_channel
35
+ @client.broadcast("foobar", "test", {})
36
+ assert false
37
+ rescue Kosmonaut::ChannelNotFoundError
38
+ end
39
+
40
+ def _test_close_channel
41
+ @client.close_channel("foo")
42
+ end
43
+
44
+ def _test_close_not_existing_channel
45
+ @client.close_channel("bar")
46
+ assert false
47
+ rescue Kosmonaut::ChannelNotFoundError
48
+ end
49
+
50
+ def _test_request_single_access_token
51
+ token = @client.request_single_access_token(".*")
52
+ assert token
53
+ assert_equal 128, token.size
54
+ end
55
+ end
@@ -0,0 +1,29 @@
1
+ require File.expand_path("../helper", __FILE__)
2
+
3
+ class MyWorker < Kosmonaut::Worker
4
+ def on_message(event, data)
5
+ puts "MSG", event, data
6
+ end
7
+
8
+ def on_error(err)
9
+ puts "ERR", err.to_s
10
+ end
11
+
12
+ def on_exception(err)
13
+ puts "EXC", err.to_s
14
+ end
15
+ end
16
+
17
+ class TestKosmonautWorker < MiniTest::Unit::TestCase
18
+ def setup
19
+ @worker = MyWorker.new("wr://#{ENV["VHOST_TOKEN"].to_s}@127.0.0.1:8081/test")
20
+ end
21
+
22
+ def test_api
23
+ Thread.new {
24
+ sleep(15)
25
+ @worker.stop
26
+ }
27
+ @worker.listen
28
+ end
29
+ end
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kosmonaut
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.2.0
6
+ platform: ruby
7
+ authors:
8
+ - Krzysztof Kowalik
9
+ - Cubox
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+
14
+ date: 2012-01-16 00:00:00 Z
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: json
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ~>
23
+ - !ruby/object:Gem::Version
24
+ version: "1.0"
25
+ type: :runtime
26
+ version_requirements: *id001
27
+ description: The WebRocket server backend client for ruby programming language
28
+ email: chris@nu7hat.ch
29
+ executables: []
30
+
31
+ extensions: []
32
+
33
+ extra_rdoc_files: []
34
+
35
+ files:
36
+ - lib/kosmonaut/client.rb
37
+ - lib/kosmonaut/errors.rb
38
+ - lib/kosmonaut/socket.rb
39
+ - lib/kosmonaut/version.rb
40
+ - lib/kosmonaut/worker.rb
41
+ - lib/kosmonaut.rb
42
+ - test/helper.rb
43
+ - test/test_kosmonaut_client.rb
44
+ - test/test_kosmonaut_worker.rb
45
+ - Rakefile
46
+ - README
47
+ - COPYING
48
+ - kosmonaut.gemspec
49
+ homepage: http://webrocket.io/
50
+ licenses:
51
+ - MIT
52
+ post_install_message:
53
+ rdoc_options: []
54
+
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: "0"
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: "0"
69
+ requirements: []
70
+
71
+ rubyforge_project:
72
+ rubygems_version: 1.8.10
73
+ signing_key:
74
+ specification_version: 3
75
+ summary: Ruby client for the WebRocket backend
76
+ test_files:
77
+ - test/helper.rb
78
+ - test/test_kosmonaut_client.rb
79
+ - test/test_kosmonaut_worker.rb