keyhole 0.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 ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 buffpojken
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.rdoc ADDED
@@ -0,0 +1,17 @@
1
+ = keyhole
2
+
3
+ Description goes here.
4
+
5
+ == Note on Patches/Pull Requests
6
+
7
+ * Fork the project.
8
+ * Make your feature addition or bug fix.
9
+ * Add tests for it. This is important so I don't break it in a
10
+ future version unintentionally.
11
+ * Commit, do not mess with rakefile, version, or history.
12
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
13
+ * Send me a pull request. Bonus points for topic branches.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2010 buffpojken. See LICENSE for details.
data/bin/keyhole ADDED
@@ -0,0 +1 @@
1
+ load File.dirname(__FILE__) + '/../lib/keyhole.rb'
@@ -0,0 +1,191 @@
1
+ require 'addressable/uri'
2
+
3
+ module EventMachine
4
+ module WebSocket
5
+ class Connection < EventMachine::Connection
6
+ include Debugger
7
+
8
+ attr_reader :state, :request
9
+
10
+ # Set the max frame lenth to very high value (10MB) until there is a
11
+ # limit specified in the spec to protect against malicious attacks
12
+ MAXIMUM_FRAME_LENGTH = 10 * 1024 * 1024
13
+
14
+ # define WebSocket callbacks
15
+ def onopen(&blk); @onopen = blk; end
16
+ def onclose(&blk); @onclose = blk; end
17
+ def onerror(&blk); @onerror = blk; end
18
+ def onmessage(&blk); @onmessage = blk; end
19
+
20
+ def initialize(options)
21
+ @options = options
22
+ @debug = options[:debug] || false
23
+ @secure = options[:secure] || false
24
+ @tls_options = options[:tls_options] || {}
25
+ @state = :handshake
26
+ @request = {}
27
+ @data = ''
28
+
29
+ debug [:initialize]
30
+ end
31
+
32
+ def post_init
33
+ start_tls(@tls_options) if @secure
34
+ end
35
+
36
+ def receive_data(data)
37
+ debug [:receive_data, data]
38
+
39
+ @data << data
40
+ dispatch
41
+ end
42
+
43
+ def unbind
44
+ debug [:unbind, :connection]
45
+
46
+ @state = :closed
47
+ @onclose.call if @onclose
48
+ end
49
+
50
+ def dispatch
51
+ case @state
52
+ when :handshake
53
+ handshake
54
+ when :connected
55
+ process_message
56
+ else raise WebSocketError, "invalid state: #{@state}"
57
+ end
58
+ end
59
+
60
+ def handshake
61
+ if @data.match(/<policy-file-request\s*\/>/)
62
+ send_flash_cross_domain_file
63
+ return false
64
+ else
65
+ debug [:inbound_headers, @data]
66
+ begin
67
+ @handler = HandlerFactory.build(@data, @secure, @debug)
68
+ @data = ''
69
+ send_data @handler.handshake
70
+
71
+ @request = @handler.request
72
+ @state = :connected
73
+ @onopen.call if @onopen
74
+ return true
75
+ rescue => e
76
+ debug [:error, e]
77
+ process_bad_request(e)
78
+ return false
79
+ end
80
+ end
81
+ end
82
+
83
+ def process_bad_request(reason)
84
+ @onerror.call(reason) if @onerror
85
+ send_data "HTTP/1.1 400 Bad request\r\n\r\n"
86
+ close_connection_after_writing
87
+ end
88
+
89
+ def send_flash_cross_domain_file
90
+ file = '<?xml version="1.0"?><cross-domain-policy><allow-access-from domain="*" to-ports="*"/></cross-domain-policy>'
91
+ debug [:cross_domain, file]
92
+ send_data file
93
+
94
+ # handle the cross-domain request transparently
95
+ # no need to notify the user about this connection
96
+ @onclose = nil
97
+ close_connection_after_writing
98
+ end
99
+
100
+ def process_message
101
+ debug [:message, @data]
102
+
103
+ # This algorithm comes straight from the spec
104
+ # http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76#section-4.2
105
+
106
+ error = false
107
+
108
+ while !error
109
+ pointer = 0
110
+ frame_type = @data[pointer].to_i
111
+ pointer += 1
112
+
113
+ if (frame_type & 0x80) == 0x80
114
+ # If the high-order bit of the /frame type/ byte is set
115
+ length = 0
116
+
117
+ loop do
118
+ b = @data[pointer].to_i
119
+ return false unless b
120
+ pointer += 1
121
+ b_v = b & 0x7F
122
+ length = length * 128 + b_v
123
+ break unless (b & 0x80) == 0x80
124
+ end
125
+
126
+ # Addition to the spec to protect against malicious requests
127
+ if length > MAXIMUM_FRAME_LENGTH
128
+ close_with_error(DataError.new("Frame length too long (#{length} bytes)"))
129
+ return false
130
+ end
131
+
132
+ if @data[pointer+length-1] == nil
133
+ debug [:buffer_incomplete, @data.inspect]
134
+ # Incomplete data - leave @data to accumulate
135
+ error = true
136
+ else
137
+ # Straight from spec - I'm sure this isn't crazy...
138
+ # 6. Read /length/ bytes.
139
+ # 7. Discard the read bytes.
140
+ @data = @data[(pointer+length)..-1]
141
+
142
+ # If the /frame type/ is 0xFF and the /length/ was 0, then close
143
+ if length == 0
144
+ send_data("\xff\x00")
145
+ @state = :closing
146
+ close_connection_after_writing
147
+ else
148
+ error = true
149
+ end
150
+ end
151
+ else
152
+ # If the high-order bit of the /frame type/ byte is _not_ set
153
+ msg = @data.slice!(/^\x00([^\xff]*)\xff/)
154
+ if msg
155
+ msg.gsub!(/\A\x00|\xff\z/, '')
156
+ if @state == :closing
157
+ debug [:ignored_message, msg]
158
+ else
159
+ msg.force_encoding('UTF-8') if msg.respond_to?(:force_encoding)
160
+ @onmessage.call(msg) if @onmessage
161
+ end
162
+ else
163
+ error = true
164
+ end
165
+ end
166
+ end
167
+
168
+ false
169
+ end
170
+
171
+ # should only be invoked after handshake, otherwise it
172
+ # will inject data into the header exchange
173
+ #
174
+ # frames need to start with 0x00-0x7f byte and end with
175
+ # an 0xFF byte. Per spec, we can also set the first
176
+ # byte to a value betweent 0x80 and 0xFF, followed by
177
+ # a leading length indicator
178
+ def send(data)
179
+ debug [:send, data]
180
+ ary = ["\x00", data, "\xff"]
181
+ ary.collect{ |s| s.force_encoding('UTF-8') if s.respond_to?(:force_encoding) }
182
+ send_data(ary.join)
183
+ end
184
+
185
+ def close_with_error(message)
186
+ @onerror.call(message) if @onerror
187
+ close_connection_after_writing
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,17 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ module Debugger
4
+
5
+ private
6
+
7
+ def debug(*data)
8
+ if @debug
9
+ require 'pp'
10
+ pp data
11
+ puts
12
+ end
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ class Handler
4
+ include Debugger
5
+
6
+ attr_reader :request
7
+
8
+ def initialize(request, response, debug = false)
9
+ @request = request
10
+ @response = response
11
+ @debug = debug
12
+ end
13
+
14
+ def handshake
15
+ # Implemented in subclass
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ class Handler75 < Handler
4
+ def handshake
5
+ location = "#{@request['Host'].scheme}://#{@request['Host'].host}"
6
+ location << ":#{@request['Host'].port}" if @request['Host'].port
7
+ location << @request['Path']
8
+
9
+ upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
10
+ upgrade << "Upgrade: WebSocket\r\n"
11
+ upgrade << "Connection: Upgrade\r\n"
12
+ upgrade << "WebSocket-Origin: #{@request['Origin']}\r\n"
13
+ upgrade << "WebSocket-Location: #{location}\r\n\r\n"
14
+
15
+ debug [:upgrade_headers, upgrade]
16
+
17
+ return upgrade
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,64 @@
1
+ require 'digest/md5'
2
+
3
+ module EventMachine
4
+ module WebSocket
5
+ class Handler76 < Handler
6
+ # "\377\000" is octet version and "\xff\x00" is hex version
7
+ TERMINATE_STRING = "\xff\x00"
8
+
9
+ def handshake
10
+ challenge_response = solve_challenge(
11
+ @request['Sec-WebSocket-Key1'],
12
+ @request['Sec-WebSocket-Key2'],
13
+ @request['Third-Key']
14
+ )
15
+
16
+ location = "#{@request['Host'].scheme}://#{@request['Host'].host}"
17
+ location << ":#{@request['Host'].port}" if @request['Host'].port
18
+ location << @request['Path']
19
+
20
+ upgrade = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
21
+ upgrade << "Upgrade: WebSocket\r\n"
22
+ upgrade << "Connection: Upgrade\r\n"
23
+ upgrade << "Sec-WebSocket-Location: #{location}\r\n"
24
+ upgrade << "Sec-WebSocket-Origin: #{@request['Origin']}\r\n"
25
+ if protocol = @request['Sec-WebSocket-Protocol']
26
+ validate_protocol!(protocol)
27
+ upgrade << "Sec-WebSocket-Protocol: #{protocol}\r\n"
28
+ end
29
+ upgrade << "\r\n"
30
+ upgrade << challenge_response
31
+
32
+ debug [:upgrade_headers, upgrade]
33
+
34
+ return upgrade
35
+ end
36
+
37
+ private
38
+
39
+ def solve_challenge(first, second, third)
40
+ # Refer to 5.2 4-9 of the draft 76
41
+ sum = [(extract_nums(first) / count_spaces(first))].pack("N*") +
42
+ [(extract_nums(second) / count_spaces(second))].pack("N*") +
43
+ third
44
+ Digest::MD5.digest(sum)
45
+ end
46
+
47
+ def extract_nums(string)
48
+ string.scan(/[0-9]/).join.to_i
49
+ end
50
+
51
+ def count_spaces(string)
52
+ spaces = string.scan(/ /).size
53
+ # As per 5.2.5, abort the connection if spaces are zero.
54
+ raise HandshakeError, "Websocket Key1 or Key2 does not contain spaces - this is a symptom of a cross-protocol attack" if spaces == 0
55
+ return spaces
56
+ end
57
+
58
+ def validate_protocol!(protocol)
59
+ raise HandshakeError, "Invalid WebSocket-Protocol: empty" if protocol.empty?
60
+ # TODO: Validate characters
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,53 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ class HandlerFactory
4
+ PATH = /^(\w+) (\/[^\s]*) HTTP\/1\.1$/
5
+ HEADER = /^([^:]+):\s*(.+)$/
6
+
7
+ def self.build(data, secure = false, debug = false)
8
+ request = {}
9
+ response = nil
10
+
11
+ lines = data.split("\r\n")
12
+
13
+ # extract request path
14
+ first_line = lines.shift.match(PATH)
15
+ raise HandshakeError, "Invalid HTTP header" unless first_line
16
+ request['Method'] = first_line[1].strip
17
+ request['Path'] = first_line[2].strip
18
+
19
+ unless request["Method"] == "GET"
20
+ raise HandshakeError, "Must be GET request"
21
+ end
22
+
23
+ # extract query string values
24
+ request['Query'] = Addressable::URI.parse(request['Path']).query_values ||= {}
25
+ # extract remaining headers
26
+ lines.each do |line|
27
+ h = HEADER.match(line)
28
+ request[h[1].strip] = h[2].strip if h
29
+ end
30
+ request['Third-Key'] = lines.last
31
+
32
+ unless request['Connection'] == 'Upgrade' and request['Upgrade'] == 'WebSocket'
33
+ raise HandshakeError, "Connection and Upgrade headers required"
34
+ end
35
+
36
+ # transform headers
37
+ protocol = (secure ? "wss" : "ws")
38
+ request['Host'] = Addressable::URI.parse("#{protocol}://"+request['Host'])
39
+
40
+ version = request['Sec-WebSocket-Key1'] ? 76 : 75
41
+
42
+ case version
43
+ when 75
44
+ Handler75.new(request, response, debug)
45
+ when 76
46
+ Handler76.new(request, response, debug)
47
+ else
48
+ raise WebSocketError, "Must not happen"
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,26 @@
1
+ module EventMachine
2
+ module WebSocket
3
+ class WebSocketError < RuntimeError; end
4
+ class HandshakeError < WebSocketError; end
5
+ class DataError < WebSocketError; end
6
+
7
+ def self.start(options, &blk)
8
+ EM.epoll
9
+ EM.run do
10
+
11
+ trap("TERM") { stop }
12
+ trap("INT") { stop }
13
+
14
+ EventMachine::start_server(options[:host], options[:port],
15
+ EventMachine::WebSocket::Connection, options) do |c|
16
+ blk.call(c)
17
+ end
18
+ end
19
+ end
20
+
21
+ def self.stop
22
+ puts "Terminating WebSocket Server"
23
+ EventMachine.stop
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,8 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
+
3
+ #require "rubygems"
4
+ require "eventmachine"
5
+
6
+ %w[ debugger websocket connection handler_factory handler handler75 handler76 ].each do |file|
7
+ require "em-websocket/#{file}"
8
+ end
data/lib/keyhole.rb ADDED
@@ -0,0 +1,42 @@
1
+ require 'rubygems'
2
+ require 'eventmachine'
3
+ require File.dirname(__FILE__) + '/sat_parser'
4
+ require File.dirname(__FILE__) + '/query_server'
5
+ require 'em-websocket'
6
+ require 'sinatra/base'
7
+ require 'thin'
8
+
9
+
10
+ EventMachine.run{
11
+
12
+ # This can implement the AJAX-based query-server, without any need
13
+ # of multiple servers. Or, it can just be an http-based reporting tool.
14
+ class Querier < Sinatra::Base
15
+ get "/" do
16
+ return "This is Keyhole, with #{$clients.length} clients connected"
17
+ end
18
+ end
19
+
20
+
21
+ $channel = EM::Channel.new
22
+ $clients = {}
23
+
24
+ EventMachine::start_server "127.0.0.1", 5000, SatParser
25
+ EventMachine::start_server "127.0.0.1", 5500, QueryServer
26
+
27
+ # TODO - Make sure this reads config from a config-file instead of this weird thing!
28
+ EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 8080, :debug => true) do |ws|
29
+ ws.onopen{
30
+ sid = $channel.subscribe { |msg| ws.send msg }
31
+ $clients[sid] = true
32
+ ws.onmessage{|msg| $channel.push "Ninja!" }
33
+ ws.onclose{
34
+ $channel.unsubscribe(sid)
35
+ $clients.delete(sid)
36
+ }
37
+ }
38
+ end
39
+
40
+ Querier.run!({:port => 3000})
41
+
42
+ }
@@ -0,0 +1,14 @@
1
+ # QueryServer responds to telnet-calls on its designated port, and responds
2
+ # with things...
3
+ class QueryServer < EventMachine::Connection
4
+
5
+ def post_init
6
+ send_data "#{$clients.length} clients connected"
7
+ end
8
+
9
+ def receive_data(data)
10
+ send_data "#{$clients.length} clients connected"
11
+ close_connection if data =~ /quit/i
12
+ end
13
+
14
+ end
data/lib/sat_parser.rb ADDED
@@ -0,0 +1,17 @@
1
+ class SatParser < EventMachine::Connection
2
+
3
+ def post_init
4
+ puts "GSAT connected"
5
+ end
6
+
7
+ def receive_data(data)
8
+ puts "Data received..."
9
+ puts data.inspect
10
+ $channel << data
11
+ end
12
+
13
+ def unbind
14
+ $channel << "GSAT closed connection"
15
+ end
16
+
17
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'keyhole'
8
+
9
+ class Test::Unit::TestCase
10
+ end
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TestKeyhole < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: keyhole
3
+ version: !ruby/object:Gem::Version
4
+ hash: 31
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 0
10
+ version: 0.0.0
11
+ platform: ruby
12
+ authors:
13
+ - buffpojken
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-09-05 00:00:00 +02:00
19
+ default_executable: keyhole
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: thoughtbot-shoulda
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :development
34
+ version_requirements: *id001
35
+ description: longer description of your gem
36
+ email: daniel@sykewarrior.com
37
+ executables:
38
+ - keyhole
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - LICENSE
43
+ - README.rdoc
44
+ files:
45
+ - lib/em-websocket.rb
46
+ - lib/em-websocket/connection.rb
47
+ - lib/em-websocket/debugger.rb
48
+ - lib/em-websocket/handler.rb
49
+ - lib/em-websocket/handler75.rb
50
+ - lib/em-websocket/handler76.rb
51
+ - lib/em-websocket/handler_factory.rb
52
+ - lib/em-websocket/websocket.rb
53
+ - lib/keyhole.rb
54
+ - lib/query_server.rb
55
+ - lib/sat_parser.rb
56
+ - LICENSE
57
+ - README.rdoc
58
+ - test/helper.rb
59
+ - test/test_keyhole.rb
60
+ - bin/keyhole
61
+ has_rdoc: true
62
+ homepage: http://github.com/buffpojken/keyhole
63
+ licenses: []
64
+
65
+ post_install_message:
66
+ rdoc_options:
67
+ - --charset=UTF-8
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ hash: 3
76
+ segments:
77
+ - 0
78
+ version: "0"
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ hash: 3
85
+ segments:
86
+ - 0
87
+ version: "0"
88
+ requirements: []
89
+
90
+ rubyforge_project:
91
+ rubygems_version: 1.3.7
92
+ signing_key:
93
+ specification_version: 3
94
+ summary: one-line summary of your gem
95
+ test_files:
96
+ - test/helper.rb
97
+ - test/test_keyhole.rb