roadrunner-live-reload 0.3.1 → 0.3.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 CHANGED
@@ -1,2 +1,2 @@
1
- roadrunner*.gem
1
+ roadrunner*.gem
2
2
  .yardoc/*
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
- source 'https://rubygems.org'
2
-
3
- gem 'rspec'
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'rspec'
4
4
  gem 'rake'
data/Gemfile.lock CHANGED
@@ -1,20 +1,20 @@
1
- GEM
2
- remote: https://rubygems.org/
3
- specs:
4
- diff-lcs (1.2.2)
5
- rake (10.0.4)
6
- rspec (2.13.0)
7
- rspec-core (~> 2.13.0)
8
- rspec-expectations (~> 2.13.0)
9
- rspec-mocks (~> 2.13.0)
10
- rspec-core (2.13.1)
11
- rspec-expectations (2.13.0)
12
- diff-lcs (>= 1.1.3, < 2.0)
13
- rspec-mocks (2.13.0)
14
-
15
- PLATFORMS
16
- ruby
17
-
18
- DEPENDENCIES
19
- rake
20
- rspec
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ diff-lcs (1.2.2)
5
+ rake (10.0.4)
6
+ rspec (2.13.0)
7
+ rspec-core (~> 2.13.0)
8
+ rspec-expectations (~> 2.13.0)
9
+ rspec-mocks (~> 2.13.0)
10
+ rspec-core (2.13.1)
11
+ rspec-expectations (2.13.0)
12
+ diff-lcs (>= 1.1.3, < 2.0)
13
+ rspec-mocks (2.13.0)
14
+
15
+ PLATFORMS
16
+ ruby
17
+
18
+ DEPENDENCIES
19
+ rake
20
+ rspec
data/LICENSE CHANGED
@@ -1,20 +1,20 @@
1
- Copyright (c) 2013 Alcides Queiroz (alcidesqueiroz@gmail.com)
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.
1
+ Copyright (c) 2013-2014 Alcides Queiroz (alcidesqueiroz@gmail.com)
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 CHANGED
@@ -1,60 +1,60 @@
1
- RoadRunner
2
- =====
3
-
4
- What is?
5
- --------
6
-
7
- RoadRunner is a simple, but powerful, live reload tool totally written in Ruby. Though RoadRunner was made in Ruby, it is language-agnostic, in simple words: you can use RoadRunner with ANY language or platform (or operating system) in your back-end. You just need ruby installed.
8
-
9
-
10
- Usage
11
- --------------------
12
- 1) Run:
13
- ```ruby
14
- sudo gem install roadrunner-live-reload
15
- ```
16
-
17
- 2) Create a roadrunner.yml file in the root folder of your project by running the command `roadrunner setup`.
18
-
19
- Your file structure will look like this:
20
- ```yaml
21
- config:
22
- polling_interval: 0.1
23
- change_check_strategy: modification_time #You also can use 'checksum'
24
- live_reload_port: 9876
25
- web_server_port: 9875
26
- files: [
27
- ["/relative/path/to/some/css-file.css", "/relative/path/in/your/web-server.css"],
28
- "/equivalent/path.css"
29
- ]
30
-
31
- ```
32
- If the relative path for a file to be monitored in your project folder structure is equal to its URL in web server, you just have to add this path (relative to the root folder) to the "files" collection.
33
-
34
- ```yaml
35
- files: [
36
- "/same/path.css",
37
- ...
38
- ]
39
- ```
40
-
41
- But if the paths are different, you can map it easily:
42
- ```yaml
43
- files: [
44
- ["/relative/path/to/some/css-file.css", "/relative/path/in/your/web-server.css"],
45
- ...
46
- ]
47
- ```
48
- 3) RoadRunner also starts a tiny web server to serve the roadrunner.js script file. So, add a reference to this file in your application layout/template/master page.
49
- ```javascript
50
- <script src="http://localhost:9875/roadrunner.js"></script>
51
- ```
52
- 4) Open a terminal, "cd" to your project root folder (where roadrunner.yml is located) and run:
53
- ```
54
- roadrunner
55
- ```
56
- 5) Open your application in your browser
57
-
58
- License
59
- -------
60
- This code is free to use under the terms of the MIT license
1
+ RoadRunner
2
+ =====
3
+
4
+ What is?
5
+ --------
6
+
7
+ RoadRunner is a simple, but powerful, live reload tool totally written in Ruby. Though RoadRunner was made in Ruby, it is language-agnostic, in simple words: you can use RoadRunner with ANY language or platform (or operating system) in your back-end. You just need ruby installed.
8
+
9
+
10
+ Usage
11
+ --------------------
12
+ 1) Run:
13
+ ```ruby
14
+ sudo gem install roadrunner-live-reload
15
+ ```
16
+
17
+ 2) Create a roadrunner.yml file in the root folder of your project by running the command `roadrunner setup`.
18
+
19
+ Your file structure will look like this:
20
+ ```yaml
21
+ config:
22
+ polling_interval: 0.1
23
+ change_check_strategy: modification_time #You also can use 'checksum'
24
+ live_reload_port: 9876
25
+ web_server_port: 9875
26
+ files: [
27
+ ["/relative/path/to/some/css-file.css", "/relative/path/in/your/web-server.css"],
28
+ "/equivalent/path.css"
29
+ ]
30
+
31
+ ```
32
+ If the relative path for a file to be monitored in your project folder structure is equal to its URL in web server, you just have to add this path (relative to the root folder) to the "files" collection.
33
+
34
+ ```yaml
35
+ files: [
36
+ "/same/path.css",
37
+ ...
38
+ ]
39
+ ```
40
+
41
+ But if the paths are different, you can map it easily:
42
+ ```yaml
43
+ files: [
44
+ ["/relative/path/to/some/css-file.css", "/relative/path/in/your/web-server.css"],
45
+ ...
46
+ ]
47
+ ```
48
+ 3) RoadRunner also starts a tiny web server to serve the roadrunner.js script file. So, add a reference to this file in your application layout/template/master page.
49
+ ```javascript
50
+ <script src="http://localhost:9875/roadrunner.js"></script>
51
+ ```
52
+ 4) Open a terminal, "cd" to your project root folder (where roadrunner.yml is located) and run:
53
+ ```
54
+ roadrunner
55
+ ```
56
+ 5) Open your application in your browser
57
+
58
+ License
59
+ -------
60
+ This code is free to use under the terms of the MIT license
data/Rakefile CHANGED
@@ -1,6 +1,12 @@
1
- #!/usr/bin/env rake
2
- require 'rubygems'
3
- require 'bundler/gem_tasks'
4
-
5
- Dir.glob(File.join(File.join(File.dirname(__FILE__), 'lib/tasks/*.rake'))).each {|rt| import rt}
1
+ #!/usr/bin/env rake
2
+ require 'rubygems'
3
+
4
+ found_gspec = Gem::Dependency.new('bundler').matching_specs.sort_by(&:version).last
5
+ unless found_gspec
6
+ puts "******Bundler not found; Install Bundler by running: sudo gem install bundler*******"
7
+ end
8
+
9
+ require 'bundler/gem_tasks'
10
+
11
+ Dir.glob(File.join(File.join(File.dirname(__FILE__), 'lib/tasks/*.rake'))).each {|rt| import rt}
6
12
  task :default => :register_roadrunner
data/distr/roadrunner CHANGED
@@ -1,29 +1,29 @@
1
- #!{RUBY_PATH}
2
- require '{ROADRUNNER_ROOT_PATH}/src/roadrunner.rb'
3
- require 'fileutils'
4
-
5
- roadrunner_root_path = "{ROADRUNNER_ROOT_PATH}"
6
-
7
- case ARGV[0]
8
- when "--help", "-h"
9
- puts "Description:"
10
- puts "\tStarts the RoadRunner server, a live reload tool which notifies listeners (ie: a web application) \n\tthat your CSS, JS, HTML files (or any file type you want) has changed.\n\n"
11
- puts "Options:"
12
- puts "%15s - The same as just \"compass\"" % "watch"
13
- puts "%15s - Creates a new roadrunner.yml config file in the current folder" % "setup"
14
- puts "%15s - Show this message" % "--help, -h"
15
- puts "%15s - Print out version information" % "--version, -v"
16
-
17
- when "--version", "-v"
18
- puts "Roadrunner Version #{RoadRunner::VERSION}\nCopyright (c) Alcides Queiroz"
19
-
20
- when "setup"
21
- FileUtils.cp("#{roadrunner_root_path}/roadrunner.sample.yml", File.join(Dir.pwd, "roadrunner.yml"))
22
- puts "Sample config file created successfully!"
23
-
24
- when "watch"
25
- RoadRunner.init_server
26
-
27
- else
28
- RoadRunner.init_server
1
+ #!{RUBY_PATH}
2
+ require '{ROADRUNNER_ROOT_PATH}/src/roadrunner.rb'
3
+ require 'fileutils'
4
+
5
+ roadrunner_root_path = "{ROADRUNNER_ROOT_PATH}"
6
+
7
+ case ARGV[0]
8
+ when "--help", "-h"
9
+ puts "Description:"
10
+ puts "\tStarts the RoadRunner server, a live reload tool which notifies listeners (e.g: a web application) \n\tthat your CSS, JS, HTML files (or any file type you want) has changed.\n\n"
11
+ puts "Options:"
12
+ puts "%15s - The same as just \"roadrunner\"" % "watch"
13
+ puts "%15s - Creates a new roadrunner.yml config file in the current folder" % "setup"
14
+ puts "%15s - Show this message" % "--help, -h"
15
+ puts "%15s - Print out version information" % "--version, -v"
16
+
17
+ when "--version", "-v"
18
+ puts "Roadrunner Version #{RoadRunner::VERSION}\nCopyright (c) Alcides Queiroz"
19
+
20
+ when "setup"
21
+ FileUtils.cp("#{roadrunner_root_path}/roadrunner.sample.yml", File.join(Dir.pwd, "roadrunner.yml"))
22
+ puts "Sample config file created successfully!"
23
+
24
+ when "watch"
25
+ RoadRunner.init_server
26
+
27
+ else
28
+ RoadRunner.init_server
29
29
  end
data/distr/roadrunner.bat CHANGED
@@ -1,6 +1,6 @@
1
- @ECHO OFF
2
- IF NOT "%~f0" == "~f0" GOTO :WinNT
3
- @"ruby.exe" "{ROADRUNNER_LOADER}" %1 %2 %3 %4 %5 %6 %7 %8 %9
4
- GOTO :EOF
5
- :WinNT
6
- @"ruby.exe" "%~dpn0" %*
1
+ @ECHO OFF
2
+ IF NOT "%~f0" == "~f0" GOTO :WinNT
3
+ @"ruby.exe" "{ROADRUNNER_LOADER}" %1 %2 %3 %4 %5 %6 %7 %8 %9
4
+ GOTO :EOF
5
+ :WinNT
6
+ @"ruby.exe" "%~dpn0" %*
data/lib/os.rb CHANGED
@@ -1,17 +1,17 @@
1
- module OS
2
- def OS.windows?
3
- (/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) != nil
4
- end
5
-
6
- def OS.mac?
7
- (/darwin/ =~ RUBY_PLATFORM) != nil
8
- end
9
-
10
- def OS.unix?
11
- !OS.windows?
12
- end
13
-
14
- def OS.linux?
15
- OS.unix? and not OS.mac?
16
- end
1
+ module OS
2
+ def OS.windows?
3
+ (/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) != nil
4
+ end
5
+
6
+ def OS.mac?
7
+ (/darwin/ =~ RUBY_PLATFORM) != nil
8
+ end
9
+
10
+ def OS.unix?
11
+ !OS.windows?
12
+ end
13
+
14
+ def OS.linux?
15
+ OS.unix? and not OS.mac?
16
+ end
17
17
  end
@@ -1,24 +1,24 @@
1
- task :register_roadrunner do
2
- ruby_path = File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name']).sub(/.*\s.*/m, '"\&"')
3
- ruby_dir = File.dirname(ruby_path)
4
- distr_files_dir = File.expand_path(File.join(File.dirname(__FILE__), "../../distr" ))
5
- roadrunner_root_path = File.expand_path(File.join(File.dirname(__FILE__), "../.." ))
6
-
7
- FileUtils.cp(File.join(distr_files_dir, "roadrunner"), ruby_dir)
8
- FileUtils.cp(File.join(distr_files_dir, "roadrunner.bat"), ruby_dir)
9
-
10
- compass_loader_path = File.join(ruby_dir, "roadrunner")
11
- compass_bat_path = File.join(ruby_dir, "roadrunner.bat")
12
-
13
- compass_loader_file_text = File.read(compass_loader_path)
14
- .gsub(/{RUBY_PATH}/, ruby_path)
15
- .gsub(/{ROADRUNNER_ROOT_PATH}/, roadrunner_root_path)
16
-
17
- compass_bat_file_text = File.read(compass_bat_path)
18
- .gsub(/{ROADRUNNER_LOADER}/, compass_loader_path)
19
-
20
- File.open(compass_loader_path, "w") { |file| file << compass_loader_file_text }
21
- File.open(compass_bat_path, "w") { |file| file << compass_bat_file_text }
22
-
23
- #`echo #{File.dirname(__FILE__)} >> d:\\projects\\roadrunner\\teste`
1
+ task :register_roadrunner do
2
+ ruby_path = File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name']).sub(/.*\s.*/m, '"\&"')
3
+ ruby_dir = File.dirname(ruby_path)
4
+ distr_files_dir = File.expand_path(File.join(File.dirname(__FILE__), "../../distr" ))
5
+ roadrunner_root_path = File.expand_path(File.join(File.dirname(__FILE__), "../.." ))
6
+
7
+ FileUtils.cp(File.join(distr_files_dir, "roadrunner"), ruby_dir)
8
+ FileUtils.cp(File.join(distr_files_dir, "roadrunner.bat"), ruby_dir)
9
+
10
+ compass_loader_path = File.join(ruby_dir, "roadrunner")
11
+ compass_bat_path = File.join(ruby_dir, "roadrunner.bat")
12
+
13
+ compass_loader_file_text = File.read(compass_loader_path)
14
+ .gsub(/{RUBY_PATH}/, ruby_path)
15
+ .gsub(/{ROADRUNNER_ROOT_PATH}/, roadrunner_root_path)
16
+
17
+ compass_bat_file_text = File.read(compass_bat_path)
18
+ .gsub(/{ROADRUNNER_LOADER}/, compass_loader_path)
19
+
20
+ File.open(compass_loader_path, "w") { |file| file << compass_loader_file_text }
21
+ File.open(compass_bat_path, "w") { |file| file << compass_bat_file_text }
22
+
23
+ #`echo #{File.dirname(__FILE__)} >> d:\\projects\\roadrunner\\teste`
24
24
  end
data/lib/version.rb CHANGED
@@ -1,3 +1,3 @@
1
- module RoadRunner
2
- VERSION = "0.3.1".freeze
3
- end
1
+ module RoadRunner
2
+ VERSION = "0.3.2".freeze
3
+ end
data/lib/websocket.rb CHANGED
@@ -1,588 +1,588 @@
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
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
588
  end