kosmonaut 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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