keyhole 0.0.0

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