shot 0.0.2 → 1.0.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/LICENSE.md ADDED
File without changes
data/README.md ADDED
@@ -0,0 +1,33 @@
1
+ Introduction
2
+ ------------
3
+
4
+ The Shot Core is responsible for implementing the base `Application` class, which processes incoming web socket
5
+ connections, turning them into instances of `ApplicationInstance`. The `Application` class also is capable of
6
+ being extended through [**Loaders**](https://github.com/shot-framework/shot-core/wiki/Loaders) and
7
+ [**Routers**](https://github.com/shot-framework/shot-core/wiki/Routers).
8
+
9
+ # Installation
10
+
11
+ ```bash
12
+ gem install shot
13
+ ```
14
+
15
+ # Usage Example
16
+
17
+ ```ruby
18
+ require 'shot'
19
+
20
+ app = Application.new
21
+
22
+ app.run do
23
+ app.on 'load' do |instance|
24
+ instance.emit 'loaded'
25
+ end
26
+ end
27
+
28
+ app.start
29
+ ```
30
+
31
+ # License
32
+
33
+ Licensed under the MIT License. For full licensing information, please see LICENSE.md.
@@ -0,0 +1,100 @@
1
+ # Application
2
+ #
3
+ # Main application instance of a Shot application. Inherits from EventdServer and implements various
4
+ # "routers", which take care of handling special Shot emissions, such as controller calling, etc.
5
+ #
6
+ # Shot Framework - Copyright (c) Jesse Aaron Dunlap <me@jessedunlap.me>
7
+ # Licensed under the MIT License. For full licensing information, please
8
+ # see LICENSE.md. http://github.com/JesseDunlap/shot/
9
+
10
+ require 'eventd'
11
+
12
+ require_relative './application_instance'
13
+
14
+ class Application < EventdServer
15
+ # Array of routers
16
+ attr_accessor :routers
17
+
18
+ # Array of loaders
19
+ attr_accessor :loaders
20
+
21
+ # Active connected clients
22
+ attr_accessor :clients
23
+
24
+ # Temporary data for storage of state-based information
25
+ attr_accessor :data
26
+
27
+ # Initialize the Application and EventdServer. Specifies a default configuration which
28
+ # allows the EventdServer to bind on http://127.0.0.1:8080
29
+
30
+ def initialize(options = { :host => '127.0.0.1', :port => 8080 })
31
+ super(options)
32
+ @routers = []
33
+ @loaders = []
34
+ @clients = []
35
+ @data = {}
36
+ setup_handlers
37
+ end
38
+
39
+ # Add a router to the list of custom routers the application supports.
40
+ #
41
+ # Note:: Existing clients will not be handled by the new router.
42
+ #
43
+ # ==Attributes
44
+ #
45
+ # * +router+ - A class which inherits from Router
46
+
47
+ def add_router(router)
48
+ @routers.push router unless @routers.include? router
49
+ end
50
+
51
+ # Add a loader to the list of custom loaders the application supports.
52
+ #
53
+ # == Attributes
54
+ #
55
+ # * +loader+ - A class which inherits from Loader
56
+
57
+ def add_loader(loader)
58
+ unless @loaders.include? loader
59
+ @loaders.push loader
60
+
61
+ @clients.each do |client|
62
+ client.add_loader loader
63
+ end
64
+ end
65
+ end
66
+
67
+ # Gets a value from the state-based store
68
+
69
+ def get(key)
70
+ @data[key]
71
+ end
72
+
73
+ # Sets a value to the state-based store
74
+
75
+ def set(key, value)
76
+ @data[key] = value
77
+ end
78
+
79
+ private
80
+
81
+ def setup_handlers
82
+ self.on 'connection' do |client|
83
+ client.on 'connect' do
84
+ application_instance = ApplicationInstance.from_eventd_client client, self
85
+
86
+ @clients.push application_instance
87
+
88
+ @loaders.each do |loader|
89
+ application_instance.add_loader loader
90
+ end
91
+
92
+ @routers.each do |router|
93
+ router.new(application_instance)
94
+ end
95
+
96
+ self.emit 'load', application_instance
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,69 @@
1
+ # ApplicationInstance
2
+ #
3
+ # Inherits from EventdClient and represents a single client in a Shot Application.
4
+ #
5
+ # Shot Framework - Copyright (c) Jesse Aaron Dunlap <me@jessedunlap.me>
6
+ # Licensed under the MIT License. For full licensing information, please
7
+ # see LICENSE.md. http://github.com/JesseDunlap/shot/
8
+
9
+ require 'eventd'
10
+
11
+ class ApplicationInstance < EventdClient
12
+ # Array of loaders
13
+ attr_accessor :loaders
14
+
15
+ # Main Application instance
16
+ attr_accessor :app
17
+
18
+ # App Configuration
19
+ attr_accessor :config
20
+
21
+ # Initialize a new ApplicationInstance from an existing EventdClient.
22
+ #
23
+ # Note:: This is the preferred way to initialize an ApplicationInstance.
24
+ #
25
+ # == Attributes
26
+ #
27
+ # * +client+ - EventdClient to copy properties from.
28
+
29
+ def self.from_eventd_client(client, app)
30
+ instance = ApplicationInstance.new
31
+ instance.app = app
32
+ instance.socket = client.socket
33
+ instance.setup_socket
34
+ instance
35
+ end
36
+
37
+ # Initialize the superclass.
38
+ #
39
+ # Note::
40
+ # Please use the ApplicationInstance#from_eventd_client method instead, since it is not usually
41
+ # a good idea to instantiate an EventdClient without a socket.
42
+
43
+ def initialize(socket = nil)
44
+ super(socket)
45
+ @config = {}
46
+ @loaders = {}
47
+ end
48
+
49
+ # Add a loader to the list of custom loaders the application supports.
50
+ #
51
+ # == Attributes
52
+ #
53
+ # * +loader+ - A class which inherits from Loader
54
+
55
+ def add_loader(loader)
56
+ loader_instance = loader.new self
57
+ @loaders[loader_instance.type] = loader_instance
58
+ end
59
+
60
+ # Calls out to any available loader and loads the specified file
61
+
62
+ def get(type, file)
63
+ if @loaders[type]
64
+ @loaders[type].get file
65
+ else
66
+ raise LoaderNotFoundException.new "Could not load type #{type}"
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,49 @@
1
+ # ConfigurationLoader
2
+ #
3
+ # Loads all configuration files from the 'application/config' directory
4
+ # if it exists. Otherwise, doesn't do anything.
5
+ #
6
+ # Shot Framework - Copyright (c) Jesse Aaron Dunlap <me@jessedunlap.me>
7
+ # Licensed under the MIT License. For full licensing information, please
8
+ # see LICENSE.md. http://github.com/JesseDunlap/shot/
9
+
10
+ require 'yaml'
11
+
12
+ require_relative './loader'
13
+
14
+ class ConfigurationLoader < Loader
15
+ def initialize(client)
16
+ super(client)
17
+ @type = 'config'
18
+ get_all
19
+ end
20
+
21
+ def get(config_file)
22
+ if config_folder_exists and config_file_exists config_file then
23
+ @client.config[config_file] = YAML.load File.read "./application/config/#{config_file}.yml"
24
+ end
25
+ end
26
+
27
+ def get_all
28
+ if config_folder_exists
29
+ config_folder = Dir.new './application/config/'
30
+
31
+ config_folder.each do |file|
32
+ if file != '.' and file != '..'
33
+ file['.yml'] = ''
34
+ get file
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def config_folder_exists
43
+ Dir.exists? './application/config'
44
+ end
45
+
46
+ def config_file_exists(file)
47
+ File.exists? "./application/config/#{file}.yml"
48
+ end
49
+ end
@@ -0,0 +1,27 @@
1
+ # Loader
2
+ #
3
+ # Interface which defines how a Loader should work, and appropriately raises exceptions for unimplemented
4
+ # requirements of a custom Loader.
5
+ #
6
+ # Shot Framework - Copyright (c) Jesse Aaron Dunlap <me@jessedunlap.me>
7
+ # Licensed under the MIT License. For full licensing information, please
8
+ # see LICENSE.md. http://github.com/JesseDunlap/shot/
9
+
10
+
11
+ require_relative './loader_exception'
12
+
13
+ class Loader
14
+ # Loader type
15
+ attr_accessor :type
16
+
17
+ # Associated EventdClient
18
+ attr_accessor :client
19
+
20
+ def initialize(client)
21
+ @client = client
22
+ end
23
+
24
+ def get(name)
25
+ raise LoaderException.new 'Custom Loader #get method has not been overridden.'
26
+ end
27
+ end
@@ -0,0 +1,14 @@
1
+ # LoaderException
2
+ #
3
+ # Custom exception raised when an issue is detected with a custom Loader
4
+ #
5
+ # Shot Framework - Copyright (c) Jesse Aaron Dunlap <me@jessedunlap.me>
6
+ # Licensed under the MIT License. For full licensing information, please
7
+ # see LICENSE.md. http://github.com/JesseDunlap/shot/
8
+
9
+
10
+ class LoaderException < Exception
11
+ def initialize(message)
12
+ super(message)
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # LoaderNotFoundException
2
+ #
3
+ # Raised if a loader is not available for a specified type
4
+ #
5
+ # Shot Framework - Copyright (c) Jesse Aaron Dunlap <me@jessedunlap.me>
6
+ # Licensed under the MIT License. For full licensing information, please
7
+ # see LICENSE.md. http://github.com/JesseDunlap/shot/
8
+
9
+
10
+ class LoaderNotFoundException < Exception
11
+ def initialize(message)
12
+ super(message)
13
+ end
14
+ end
@@ -0,0 +1,26 @@
1
+ # Router
2
+ #
3
+ # Defines the basic structure of a Router. Routers allow for custom behavior for incoming EventdClients when
4
+ # they connect to the Shot Application. Routers should, at the very least, implement the Router#initialize
5
+ # method, which will be passed an instance of the client the router is handling.
6
+ #
7
+ # Shot Framework - Copyright (c) Jesse Aaron Dunlap <me@jessedunlap.me>
8
+ # Licensed under the MIT License. For full licensing information, please
9
+ # see LICENSE.md. http://github.com/JesseDunlap/shot/
10
+
11
+
12
+ class Router
13
+
14
+ # Associated EventdClient
15
+ attr_accessor :client
16
+
17
+ # Initialize the Router, and save the passed client
18
+ #
19
+ # == Attributes
20
+ #
21
+ # * +client+ - Instance of EventdClient, which the router should handle
22
+
23
+ def initialize(client)
24
+ @client = client
25
+ end
26
+ end
data/lib/shot/shot.rb ADDED
@@ -0,0 +1,20 @@
1
+ # What is this item?
2
+ #
3
+ # What does this item do?
4
+ #
5
+ # Shot Framework - Copyright (c) Jesse Aaron Dunlap <me@jessedunlap.me>
6
+ # Licensed under the MIT License. For full licensing information, please
7
+ # see LICENSE.md. http://github.com/JesseDunlap/shot/
8
+
9
+
10
+ class Shot
11
+ # Configure paths for easier load and require access to the lib and application folders
12
+
13
+ def self.setup
14
+ $:.unshift(File.expand_path('lib/', __FILE__.gsub('app.rb', '')))
15
+ $:.unshift(File.expand_path('application/', __FILE__.gsub('app.rb', '')))
16
+
17
+ $LOAD_PATH.unshift(File.expand_path('lib/', __FILE__.gsub('app.rb', '')))
18
+ $LOAD_PATH.unshift(File.expand_path('application/', __FILE__.gsub('app.rb', '')))
19
+ end
20
+ end
data/lib/shot.rb CHANGED
@@ -1,2 +1,7 @@
1
- require_relative '../lib/shot/shot_server'
2
- require_relative '../lib/shot/shot_client'
1
+ require_relative './shot/shot'
2
+ require_relative './shot/application'
3
+ require_relative './shot/router'
4
+ require_relative './shot/loader'
5
+ require_relative './shot/loader_exception'
6
+ require_relative './shot/loader_not_found_exception'
7
+ require_relative './shot/configuration_loader'
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 1.0.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
8
- - Jesse Dunlap
8
+ - Jesse A. Dunlap
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-05-18 00:00:00.000000000 Z
12
+ date: 2013-06-30 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: eventd
@@ -27,18 +27,43 @@ dependencies:
27
27
  - - ! '>='
28
28
  - !ruby/object:Gem::Version
29
29
  version: '0'
30
- description: Shot provides an event based WebSocket client and server for Ruby.
30
+ - !ruby/object:Gem::Dependency
31
+ name: flexmock
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: Shot Core provides a fundamental starting point for all Shot applications
47
+ by implementing an EventdServer, and a series of extensible systems (Routers & Loaders).
31
48
  email: me@jessedunlap.me
32
49
  executables: []
33
50
  extensions: []
34
51
  extra_rdoc_files: []
35
52
  files:
53
+ - lib/shot/application.rb
54
+ - lib/shot/application_instance.rb
55
+ - lib/shot/configuration_loader.rb
56
+ - lib/shot/loader.rb
57
+ - lib/shot/loader_exception.rb
58
+ - lib/shot/loader_not_found_exception.rb
59
+ - lib/shot/router.rb
60
+ - lib/shot/shot.rb
36
61
  - lib/shot.rb
37
- - lib/shot/shot_server.rb
38
- - lib/shot/shot_client.rb
39
- - lib/shot/web_socket.rb
40
- homepage: ''
41
- licenses: []
62
+ - LICENSE.md
63
+ - README.md
64
+ homepage: http://github.com/shot/
65
+ licenses:
66
+ - MIT
42
67
  post_install_message:
43
68
  rdoc_options: []
44
69
  require_paths:
@@ -60,5 +85,5 @@ rubyforge_project:
60
85
  rubygems_version: 1.8.24
61
86
  signing_key:
62
87
  specification_version: 3
63
- summary: An event based WebSocket layer.
88
+ summary: The core of Shot, a real time applciation framework written in Ruby.
64
89
  test_files: []
@@ -1,44 +0,0 @@
1
- require 'json'
2
- require 'eventd'
3
-
4
- ##
5
- # An enhanced event-based client. Each connection to a MagicSocketServer initializes
6
- # a new instance of MagicSocketClient for easier interaction.
7
- class MagicSocketClient < Eventd
8
- ##
9
- # MagicSocketClient constructor
10
- def initialize(socket)
11
- super()
12
- @socket = socket
13
-
14
- @socket.onmessage do |message|
15
- begin
16
- json = JSON.parse message
17
-
18
- event = json['event']
19
- parameters = json['parameters']
20
-
21
- # Passing false as third parameter to prevent this being sent
22
- # back over the socket to the client
23
- self.emit event, parameters, false
24
- self.emit 'any', { :event => event, :parameters => parameters }, false
25
- rescue Exception => e
26
- $stderr.puts e
27
- $stderr.puts e.backtrace
28
- end
29
- end
30
-
31
- @socket.onclose do |e|
32
- self.emit 'disconnect', e
33
- end
34
- end
35
-
36
- def emit(event_name, parameters, allow_socket = true)
37
- if allow_socket
38
- json = { :event => event_name, :parameters => parameters }
39
- @socket.send json.to_json
40
- end
41
-
42
- super(event_name, parameters)
43
- end
44
- end
@@ -1,59 +0,0 @@
1
- require 'json'
2
- require 'eventd'
3
- require 'em-websocket'
4
- require_relative 'shot_client'
5
-
6
- ##
7
- # Implements an event-based WebSocket server that accepts client connections
8
- # utilizing the WebSocket protocol. Uses the em-websocket library to create
9
- # a server, accept connections, and handshake with them.
10
- #
11
- # Author:: Jesse A. Dunlap <me@jessedunlap.me>
12
- # License:: MIT
13
-
14
- class ShotServer < Eventd
15
- ##
16
- # MagicSocketServer constructor
17
- def initialize(host = '127.0.0.1', port = 8080)
18
- super()
19
-
20
- @configuration = {
21
- :host => host,
22
- :port => port
23
- }
24
-
25
- @clients = []
26
- end
27
-
28
- ##
29
- # Merges a new configuration object with the existing default configuration.
30
- # Configuration is passed into the EM::WebSocket.run method. This method can
31
- # be used to set up SSL and other em-websocket configuration.
32
- def configure(configuration)
33
- @configuration = @configuration.merge configuration
34
- end
35
-
36
- ##
37
- # Calls the specified callback when the server is running, so that you can then
38
- # handle connections properly.
39
- def run(&callback)
40
- @run = callback
41
- end
42
-
43
- ##
44
- # Binds the websocket connection to the configured host and port, and begins
45
- # handling incoming connections. NOTE: It is important to call this method
46
- # **after** you implement the run method. No code will execute past the point
47
- # at which you call listen.
48
- def listen
49
- EM.run do
50
- if @run then @run.call end
51
-
52
- EM::WebSocket.run(@configuration) do |ws|
53
- ws.onopen do |handshake|
54
- self.emit 'connection', MagicSocketClient.new(ws)
55
- end
56
- end
57
- end
58
- end
59
- end
@@ -1,588 +0,0 @@
1
- # Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
2
- # Lincense: New BSD Lincense
3
- # Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75
4
- # Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
5
- # Reference: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07
6
- # Reference: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10
7
-
8
- require "base64"
9
- require "socket"
10
- require "uri"
11
- require "digest/md5"
12
- require "digest/sha1"
13
- require "openssl"
14
- require "stringio"
15
-
16
-
17
- class WebSocket
18
-
19
- class << self
20
-
21
- attr_accessor(:debug)
22
-
23
- end
24
-
25
- class Error < RuntimeError
26
-
27
- end
28
-
29
- WEB_SOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
30
- OPCODE_CONTINUATION = 0x00
31
- OPCODE_TEXT = 0x01
32
- OPCODE_BINARY = 0x02
33
- OPCODE_CLOSE = 0x08
34
- OPCODE_PING = 0x09
35
- OPCODE_PONG = 0x0a
36
-
37
- def initialize(arg, params = {})
38
- if params[:server] # server
39
-
40
- @server = params[:server]
41
- @socket = arg
42
- line = gets()
43
- if !line
44
- raise(WebSocket::Error, "Client disconnected without sending anything.")
45
- end
46
- line = line.chomp()
47
- if !(line =~ /\AGET (\S+) HTTP\/1.1\z/n)
48
- raise(WebSocket::Error, "Invalid request: #{line}")
49
- end
50
- @path = $1
51
- read_header()
52
- if @header["sec-websocket-version"]
53
- @web_socket_version = @header["sec-websocket-version"]
54
- @key3 = nil
55
- elsif @header["sec-websocket-key1"] && @header["sec-websocket-key2"]
56
- @web_socket_version = "hixie-76"
57
- @key3 = read(8)
58
- else
59
- @web_socket_version = "hixie-75"
60
- @key3 = nil
61
- end
62
- if !@server.accepted_origin?(self.origin)
63
- raise(WebSocket::Error,
64
- ("Unaccepted origin: %s (server.accepted_domains = %p)\n\n" +
65
- "To accept this origin, write e.g. \n" +
66
- " WebSocketServer.new(..., :accepted_domains => [%p]), or\n" +
67
- " WebSocketServer.new(..., :accepted_domains => [\"*\"])\n") %
68
- [self.origin, @server.accepted_domains, @server.origin_to_domain(self.origin)])
69
- end
70
- @handshaked = false
71
-
72
- else # client
73
-
74
- @web_socket_version = "hixie-76"
75
- uri = arg.is_a?(String) ? URI.parse(arg) : arg
76
-
77
- if uri.scheme == "ws"
78
- default_port = 80
79
- elsif uri.scheme = "wss"
80
- default_port = 443
81
- else
82
- raise(WebSocket::Error, "unsupported scheme: #{uri.scheme}")
83
- end
84
-
85
- @path = (uri.path.empty? ? "/" : uri.path) + (uri.query ? "?" + uri.query : "")
86
- host = uri.host + ((!uri.port || uri.port == default_port) ? "" : ":#{uri.port}")
87
- origin = params[:origin] || "http://#{uri.host}"
88
- key1 = generate_key()
89
- key2 = generate_key()
90
- key3 = generate_key3()
91
-
92
- socket = TCPSocket.new(uri.host, uri.port || default_port)
93
-
94
- if uri.scheme == "ws"
95
- @socket = socket
96
- else
97
- @socket = ssl_handshake(socket)
98
- end
99
-
100
- write(
101
- "GET #{@path} HTTP/1.1\r\n" +
102
- "Upgrade: WebSocket\r\n" +
103
- "Connection: Upgrade\r\n" +
104
- "Host: #{host}\r\n" +
105
- "Origin: #{origin}\r\n" +
106
- "Sec-WebSocket-Key1: #{key1}\r\n" +
107
- "Sec-WebSocket-Key2: #{key2}\r\n" +
108
- "\r\n" +
109
- "#{key3}")
110
- flush()
111
-
112
- line = gets().chomp()
113
- raise(WebSocket::Error, "bad response: #{line}") if !(line =~ /\AHTTP\/1.1 101 /n)
114
- read_header()
115
- if (@header["sec-websocket-origin"] || "").downcase() != origin.downcase()
116
- raise(WebSocket::Error,
117
- "origin doesn't match: '#{@header["sec-websocket-origin"]}' != '#{origin}'")
118
- end
119
- reply_digest = read(16)
120
- expected_digest = hixie_76_security_digest(key1, key2, key3)
121
- if reply_digest != expected_digest
122
- raise(WebSocket::Error,
123
- "security digest doesn't match: %p != %p" % [reply_digest, expected_digest])
124
- end
125
- @handshaked = true
126
-
127
- end
128
- @received = []
129
- @buffer = ""
130
- @closing_started = false
131
- end
132
-
133
- attr_reader(:server, :header, :path)
134
-
135
- def handshake(status = nil, header = {})
136
- if @handshaked
137
- raise(WebSocket::Error, "handshake has already been done")
138
- end
139
- status ||= "101 Switching Protocols"
140
- def_header = {}
141
- case @web_socket_version
142
- when "hixie-75"
143
- def_header["WebSocket-Origin"] = self.origin
144
- def_header["WebSocket-Location"] = self.location
145
- extra_bytes = ""
146
- when "hixie-76"
147
- def_header["Sec-WebSocket-Origin"] = self.origin
148
- def_header["Sec-WebSocket-Location"] = self.location
149
- extra_bytes = hixie_76_security_digest(
150
- @header["Sec-WebSocket-Key1"], @header["Sec-WebSocket-Key2"], @key3)
151
- else
152
- def_header["Sec-WebSocket-Accept"] = security_digest(@header["sec-websocket-key"])
153
- extra_bytes = ""
154
- end
155
- header = def_header.merge(header)
156
- header_str = header.map(){ |k, v| "#{k}: #{v}\r\n" }.join("")
157
- # Note that Upgrade and Connection must appear in this order.
158
- write(
159
- "HTTP/1.1 #{status}\r\n" +
160
- "Upgrade: websocket\r\n" +
161
- "Connection: Upgrade\r\n" +
162
- "#{header_str}\r\n#{extra_bytes}")
163
- flush()
164
- @handshaked = true
165
- end
166
-
167
- def send(data)
168
- if !@handshaked
169
- raise(WebSocket::Error, "call WebSocket\#handshake first")
170
- end
171
- case @web_socket_version
172
- when "hixie-75", "hixie-76"
173
- data = force_encoding(data.dup(), "ASCII-8BIT")
174
- write("\x00#{data}\xff")
175
- flush()
176
- else
177
- send_frame(OPCODE_TEXT, data, !@server)
178
- end
179
- end
180
-
181
- def receive()
182
- if !@handshaked
183
- raise(WebSocket::Error, "call WebSocket\#handshake first")
184
- end
185
- case @web_socket_version
186
-
187
- when "hixie-75", "hixie-76"
188
- packet = gets("\xff")
189
- return nil if !packet
190
- if packet =~ /\A\x00(.*)\xff\z/nm
191
- return force_encoding($1, "UTF-8")
192
- elsif packet == "\xff" && read(1) == "\x00" # closing
193
- close(1005, "", :peer)
194
- return nil
195
- else
196
- raise(WebSocket::Error, "input must be either '\\x00...\\xff' or '\\xff\\x00'")
197
- end
198
-
199
- else
200
- begin
201
- bytes = read(2).unpack("C*")
202
- fin = (bytes[0] & 0x80) != 0
203
- opcode = bytes[0] & 0x0f
204
- mask = (bytes[1] & 0x80) != 0
205
- plength = bytes[1] & 0x7f
206
- if plength == 126
207
- bytes = read(2)
208
- plength = bytes.unpack("n")[0]
209
- elsif plength == 127
210
- bytes = read(8)
211
- (high, low) = bytes.unpack("NN")
212
- plength = high * (2 ** 32) + low
213
- end
214
- if @server && !mask
215
- # Masking is required.
216
- @socket.close()
217
- raise(WebSocket::Error, "received unmasked data")
218
- end
219
- mask_key = mask ? read(4).unpack("C*") : nil
220
- payload = read(plength)
221
- payload = apply_mask(payload, mask_key) if mask
222
- case opcode
223
- when OPCODE_TEXT
224
- return force_encoding(payload, "UTF-8")
225
- when OPCODE_BINARY
226
- raise(WebSocket::Error, "received binary data, which is not supported")
227
- when OPCODE_CLOSE
228
- close(1005, "", :peer)
229
- return nil
230
- when OPCODE_PING
231
- raise(WebSocket::Error, "received ping, which is not supported")
232
- when OPCODE_PONG
233
- else
234
- raise(WebSocket::Error, "received unknown opcode: %d" % opcode)
235
- end
236
- rescue EOFError
237
- return nil
238
- end
239
-
240
- end
241
- end
242
-
243
- def tcp_socket
244
- return @socket
245
- end
246
-
247
- def host
248
- return @header["host"]
249
- end
250
-
251
- def origin
252
- case @web_socket_version
253
- when "7", "8"
254
- name = "sec-websocket-origin"
255
- else
256
- name = "origin"
257
- end
258
- if @header[name]
259
- return @header[name]
260
- else
261
- raise(WebSocket::Error, "%s header is missing" % name)
262
- end
263
- end
264
-
265
- def location
266
- return "ws://#{self.host}#{@path}"
267
- end
268
-
269
- # Does closing handshake.
270
- def close(code = 1005, reason = "", origin = :self)
271
- if !@closing_started
272
- case @web_socket_version
273
- when "hixie-75", "hixie-76"
274
- write("\xff\x00")
275
- else
276
- if code == 1005
277
- payload = ""
278
- else
279
- payload = [code].pack("n") + force_encoding(reason.dup(), "ASCII-8BIT")
280
- end
281
- send_frame(OPCODE_CLOSE, payload, false)
282
- end
283
- end
284
- @socket.close() if origin == :peer
285
- @closing_started = true
286
- end
287
-
288
- def close_socket()
289
- @socket.close()
290
- end
291
-
292
- private
293
-
294
- NOISE_CHARS = ("\x21".."\x2f").to_a() + ("\x3a".."\x7e").to_a()
295
-
296
- def read_header()
297
- @header = {}
298
- while line = gets()
299
- line = line.chomp()
300
- break if line.empty?
301
- if !(line =~ /\A(\S+): (.*)\z/n)
302
- raise(WebSocket::Error, "invalid request: #{line}")
303
- end
304
- @header[$1] = $2
305
- @header[$1.downcase()] = $2
306
- end
307
- if !@header["upgrade"]
308
- raise(WebSocket::Error, "Upgrade header is missing")
309
- end
310
- if !(@header["upgrade"] =~ /\AWebSocket\z/i)
311
- raise(WebSocket::Error, "invalid Upgrade: " + @header["upgrade"])
312
- end
313
- if !@header["connection"]
314
- raise(WebSocket::Error, "Connection header is missing")
315
- end
316
- if @header["connection"].split(/,/).grep(/\A\s*Upgrade\s*\z/i).empty?
317
- raise(WebSocket::Error, "invalid Connection: " + @header["connection"])
318
- end
319
- end
320
-
321
- def send_frame(opcode, payload, mask)
322
- payload = force_encoding(payload.dup(), "ASCII-8BIT")
323
- # Setting StringIO's encoding to ASCII-8BIT.
324
- buffer = StringIO.new(force_encoding("", "ASCII-8BIT"))
325
- write_byte(buffer, 0x80 | opcode)
326
- masked_byte = mask ? 0x80 : 0x00
327
- if payload.bytesize <= 125
328
- write_byte(buffer, masked_byte | payload.bytesize)
329
- elsif payload.bytesize < 2 ** 16
330
- write_byte(buffer, masked_byte | 126)
331
- buffer.write([payload.bytesize].pack("n"))
332
- else
333
- write_byte(buffer, masked_byte | 127)
334
- buffer.write([payload.bytesize / (2 ** 32), payload.bytesize % (2 ** 32)].pack("NN"))
335
- end
336
- if mask
337
- mask_key = Array.new(4){ rand(256) }
338
- buffer.write(mask_key.pack("C*"))
339
- payload = apply_mask(payload, mask_key)
340
- end
341
- buffer.write(payload)
342
- write(buffer.string)
343
- end
344
-
345
- def gets(rs = $/)
346
- line = @socket.gets(rs)
347
- $stderr.printf("recv> %p\n", line) if WebSocket.debug
348
- return line
349
- end
350
-
351
- def read(num_bytes)
352
- str = @socket.read(num_bytes)
353
- $stderr.printf("recv> %p\n", str) if WebSocket.debug
354
- if str && str.bytesize == num_bytes
355
- return str
356
- else
357
- raise(EOFError)
358
- end
359
- end
360
-
361
- def write(data)
362
- if WebSocket.debug
363
- data.scan(/\G(.*?(\n|\z))/n) do
364
- $stderr.printf("send> %p\n", $&) if !$&.empty?
365
- end
366
- end
367
- @socket.write(data)
368
- end
369
-
370
- def flush()
371
- @socket.flush()
372
- end
373
-
374
- def write_byte(buffer, byte)
375
- buffer.write([byte].pack("C"))
376
- end
377
-
378
- def security_digest(key)
379
- return Base64.encode64(Digest::SHA1.digest(key + WEB_SOCKET_GUID)).gsub(/\n/, "")
380
- end
381
-
382
- def hixie_76_security_digest(key1, key2, key3)
383
- bytes1 = websocket_key_to_bytes(key1)
384
- bytes2 = websocket_key_to_bytes(key2)
385
- return Digest::MD5.digest(bytes1 + bytes2 + key3)
386
- end
387
-
388
- def apply_mask(payload, mask_key)
389
- orig_bytes = payload.unpack("C*")
390
- new_bytes = []
391
- orig_bytes.each_with_index() do |b, i|
392
- new_bytes.push(b ^ mask_key[i % 4])
393
- end
394
- return new_bytes.pack("C*")
395
- end
396
-
397
- def generate_key()
398
- spaces = 1 + rand(12)
399
- max = 0xffffffff / spaces
400
- number = rand(max + 1)
401
- key = (number * spaces).to_s()
402
- (1 + rand(12)).times() do
403
- char = NOISE_CHARS[rand(NOISE_CHARS.size)]
404
- pos = rand(key.size + 1)
405
- key[pos...pos] = char
406
- end
407
- spaces.times() do
408
- pos = 1 + rand(key.size - 1)
409
- key[pos...pos] = " "
410
- end
411
- return key
412
- end
413
-
414
- def generate_key3()
415
- return [rand(0x100000000)].pack("N") + [rand(0x100000000)].pack("N")
416
- end
417
-
418
- def websocket_key_to_bytes(key)
419
- num = key.gsub(/[^\d]/n, "").to_i() / key.scan(/ /).size
420
- return [num].pack("N")
421
- end
422
-
423
- def force_encoding(str, encoding)
424
- if str.respond_to?(:force_encoding)
425
- return str.force_encoding(encoding)
426
- else
427
- return str
428
- end
429
- end
430
-
431
- def ssl_handshake(socket)
432
- ssl_context = OpenSSL::SSL::SSLContext.new()
433
- ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
434
- ssl_socket.sync_close = true
435
- ssl_socket.connect()
436
- return ssl_socket
437
- end
438
-
439
- end
440
-
441
-
442
- class WebSocketServer
443
-
444
- def initialize(params_or_uri, params = nil)
445
- if params
446
- uri = params_or_uri.is_a?(String) ? URI.parse(params_or_uri) : params_or_uri
447
- params[:port] ||= uri.port
448
- params[:accepted_domains] ||= [uri.host]
449
- else
450
- params = params_or_uri
451
- end
452
- @port = params[:port] || 80
453
- @accepted_domains = params[:accepted_domains]
454
- if !@accepted_domains
455
- raise(ArgumentError, "params[:accepted_domains] is required")
456
- end
457
- if params[:host]
458
- @tcp_server = TCPServer.open(params[:host], @port)
459
- else
460
- @tcp_server = TCPServer.open(@port)
461
- end
462
- end
463
-
464
- attr_reader(:tcp_server, :port, :accepted_domains)
465
-
466
- def run(&block)
467
- while true
468
- Thread.start(accept()) do |s|
469
- begin
470
- ws = create_web_socket(s)
471
- yield(ws) if ws
472
- rescue => ex
473
- print_backtrace(ex)
474
- ensure
475
- begin
476
- ws.close_socket() if ws
477
- rescue
478
- end
479
- end
480
- end
481
- end
482
- end
483
-
484
- def accept()
485
- return @tcp_server.accept()
486
- end
487
-
488
- def accepted_origin?(origin)
489
- domain = origin_to_domain(origin)
490
- return @accepted_domains.any?(){ |d| File.fnmatch(d, domain) }
491
- end
492
-
493
- def origin_to_domain(origin)
494
- if origin == "null" || origin == "file://" # local file
495
- return "null"
496
- else
497
- return URI.parse(origin).host
498
- end
499
- end
500
-
501
- def create_web_socket(socket)
502
- ch = socket.getc()
503
- if ch == ?<
504
- # This is Flash socket policy file request, not an actual Web Socket connection.
505
- send_flash_socket_policy_file(socket)
506
- return nil
507
- else
508
- socket.ungetc(ch) if ch
509
- return WebSocket.new(socket, :server => self)
510
- end
511
- end
512
-
513
- private
514
-
515
- def print_backtrace(ex)
516
- $stderr.printf("%s: %s (%p)\n", ex.backtrace[0], ex.message, ex.class)
517
- for s in ex.backtrace[1..-1]
518
- $stderr.printf(" %s\n", s)
519
- end
520
- end
521
-
522
- # Handles Flash socket policy file request sent when web-socket-js is used:
523
- # http://github.com/gimite/web-socket-js/tree/master
524
- def send_flash_socket_policy_file(socket)
525
- socket.puts('<?xml version="1.0"?>')
526
- socket.puts('<!DOCTYPE cross-domain-policy SYSTEM ' +
527
- '"http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">')
528
- socket.puts('<cross-domain-policy>')
529
- for domain in @accepted_domains
530
- next if domain == "file://"
531
- socket.puts("<allow-access-from domain=\"#{domain}\" to-ports=\"#{@port}\"/>")
532
- end
533
- socket.puts('</cross-domain-policy>')
534
- socket.close()
535
- end
536
-
537
- end
538
-
539
-
540
- if __FILE__ == $0
541
- Thread.abort_on_exception = true
542
-
543
- if ARGV[0] == "server" && ARGV.size == 3
544
-
545
- server = WebSocketServer.new(
546
- :accepted_domains => [ARGV[1]],
547
- :port => ARGV[2].to_i())
548
- puts("Server is running at port %d" % server.port)
549
- server.run() do |ws|
550
- puts("Connection accepted")
551
- puts("Path: #{ws.path}, Origin: #{ws.origin}")
552
- if ws.path == "/"
553
- ws.handshake()
554
- while data = ws.receive()
555
- printf("Received: %p\n", data)
556
- ws.send(data)
557
- printf("Sent: %p\n", data)
558
- end
559
- else
560
- ws.handshake("404 Not Found")
561
- end
562
- puts("Connection closed")
563
- end
564
-
565
- elsif ARGV[0] == "client" && ARGV.size == 2
566
-
567
- client = WebSocket.new(ARGV[1])
568
- puts("Connected")
569
- Thread.new() do
570
- while data = client.receive()
571
- printf("Received: %p\n", data)
572
- end
573
- end
574
- $stdin.each_line() do |line|
575
- data = line.chomp()
576
- client.send(data)
577
- printf("Sent: %p\n", data)
578
- end
579
-
580
- else
581
-
582
- $stderr.puts("Usage:")
583
- $stderr.puts(" ruby web_socket.rb server ACCEPTED_DOMAIN PORT")
584
- $stderr.puts(" ruby web_socket.rb client ws://HOST:PORT/")
585
- exit(1)
586
-
587
- end
588
- end