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 +20 -0
- data/README.rdoc +17 -0
- data/bin/keyhole +1 -0
- data/lib/em-websocket/connection.rb +191 -0
- data/lib/em-websocket/debugger.rb +17 -0
- data/lib/em-websocket/handler.rb +19 -0
- data/lib/em-websocket/handler75.rb +21 -0
- data/lib/em-websocket/handler76.rb +64 -0
- data/lib/em-websocket/handler_factory.rb +53 -0
- data/lib/em-websocket/websocket.rb +26 -0
- data/lib/em-websocket.rb +8 -0
- data/lib/keyhole.rb +42 -0
- data/lib/query_server.rb +14 -0
- data/lib/sat_parser.rb +17 -0
- data/test/helper.rb +10 -0
- data/test/test_keyhole.rb +7 -0
- metadata +97 -0
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,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
|
data/lib/em-websocket.rb
ADDED
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
|
+
}
|
data/lib/query_server.rb
ADDED
@@ -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
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
|