google_anymote 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +66 -0
- data/Rakefile +18 -0
- data/bin/pair +29 -0
- data/google_anymote.gemspec +21 -0
- data/lib/google_anymote/exceptions.rb +7 -0
- data/lib/google_anymote/pair.rb +150 -0
- data/lib/google_anymote/tv.rb +96 -0
- data/lib/google_anymote/version.rb +4 -0
- data/lib/google_anymote.rb +7 -0
- data/lib/proto/keycodes.pb.rb +359 -0
- data/lib/proto/keycodes.proto +190 -0
- data/lib/proto/polo.pb.rb +211 -0
- data/lib/proto/polo.proto +124 -0
- data/lib/proto/remote.pb.rb +200 -0
- data/lib/proto/remote.proto +128 -0
- metadata +115 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use 1.9.3@google_anymote
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Richard Hurt
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# Ruby Gem for the Google TV Pairing and Anymote Protocols
|
2
|
+
|
3
|
+
This gem implements the Google Anymote Protocol which is used to send events to Google TVs.
|
4
|
+
The protocol is based on a client-server model, with communications based on protocol buffers.
|
5
|
+
Clients search for a server on the local network. When a client wants to connect to a server
|
6
|
+
it has discovered, it does pairing authentication. After a successful pairing, both the client
|
7
|
+
and the server have certificates specific to the client app, and can communicate in the future
|
8
|
+
without the need to authenticate again. The transport layer uses TSL/SSL to protect messages
|
9
|
+
against sniffing.
|
10
|
+
|
11
|
+
Note: I couldn't have made this without [Steven Le's Python client](https://github.com/stevenle/googletv-anymote).
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
gem 'google_anymote'
|
18
|
+
|
19
|
+
And then execute:
|
20
|
+
|
21
|
+
$ bundle
|
22
|
+
|
23
|
+
Or install it yourself as:
|
24
|
+
|
25
|
+
$ gem install google_anymote
|
26
|
+
|
27
|
+
## Prerequisites
|
28
|
+
|
29
|
+
In order to send commands to a Google TV you must have an OpenSSL certificate. This certificate
|
30
|
+
can be self-signed and is pretty easy to generate. Just follow these steps
|
31
|
+
(taken from http://www.akadia.com/services/ssh_test_certificate.html):
|
32
|
+
|
33
|
+
$ openssl genrsa -des3 -out server.key 1024
|
34
|
+
$ openssl req -new -key server.key -out server.csr
|
35
|
+
$ openssl rsa -in server.key -out server.key
|
36
|
+
$ openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
|
37
|
+
$ cat server.key server.crt > cert.pem
|
38
|
+
|
39
|
+
Use the `cert.pem` as your certificate when you pair with the Google TV.
|
40
|
+
|
41
|
+
## Usage
|
42
|
+
|
43
|
+
### Command Line Utilities
|
44
|
+
|
45
|
+
This gem includes several command line utilities to get you up and running.
|
46
|
+
|
47
|
+
* <del>discover - searches your network for compatible Google TVs</del> - coming soon
|
48
|
+
* pair - helps you pair your computer to a particular Google TV
|
49
|
+
|
50
|
+
### As a gem
|
51
|
+
|
52
|
+
1. Create a GoogleAnymote::TV object
|
53
|
+
|
54
|
+
gtv = GoogleAnymote::TV.new(my_cert, hostname)
|
55
|
+
|
56
|
+
2. Fling URIs to that TV
|
57
|
+
|
58
|
+
gtv.fling_uri('http://github.com')
|
59
|
+
|
60
|
+
## Contributing
|
61
|
+
|
62
|
+
1. Fork it
|
63
|
+
2. Create your feature branch ('git checkout -b my-new-feature')
|
64
|
+
3. Commit your changes ('git commit -am 'Added some feature')
|
65
|
+
4. Push to the branch ('git push origin my-new-feature')
|
66
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
|
4
|
+
desc "Generate documentation"
|
5
|
+
task :doc => :yard
|
6
|
+
|
7
|
+
desc "Generated YARD documentation"
|
8
|
+
task :yard do
|
9
|
+
require "yard"
|
10
|
+
|
11
|
+
opts = []
|
12
|
+
opts.push("--protected")
|
13
|
+
opts.push("--no-private")
|
14
|
+
opts.push("--private")
|
15
|
+
opts.push("--title", "GoogleAnymote")
|
16
|
+
|
17
|
+
YARD::CLI::Yardoc.run(*opts)
|
18
|
+
end
|
data/bin/pair
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'google_anymote'
|
3
|
+
|
4
|
+
unless ARGV.count == 3
|
5
|
+
abort "Usage: pair name host_name certificate\n\n"
|
6
|
+
end
|
7
|
+
|
8
|
+
# Collect arguments
|
9
|
+
name = ARGV.shift
|
10
|
+
host = ARGV.shift
|
11
|
+
cert = File.read ARGV.shift
|
12
|
+
port = 9551 + 1
|
13
|
+
|
14
|
+
# Make a connection to the TV
|
15
|
+
pair = GoogleAnymote::Pair.new(cert, host, name)
|
16
|
+
|
17
|
+
# Ask the TV to pair
|
18
|
+
pair.start_pairing
|
19
|
+
|
20
|
+
# Collect pairing code
|
21
|
+
print 'Enter the code from the TV: '
|
22
|
+
code = gets.chomp
|
23
|
+
|
24
|
+
# Complete the pairing process
|
25
|
+
begin
|
26
|
+
pair.complete_pairing(code)
|
27
|
+
rescue Exception => e
|
28
|
+
abort e
|
29
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/google_anymote/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Richard Hurt"]
|
6
|
+
gem.email = ["rnhurt@gmail.com"]
|
7
|
+
gem.description = %q{Ruby implementation of the Google Anymote Protocol.}
|
8
|
+
gem.summary = %q{This library uses the Google Anymote protocol to send events to Google TV servers.}
|
9
|
+
gem.homepage = "https://github.com/rnhurt/google_anymote"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "google_anymote"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = GoogleAnymote::VERSION
|
17
|
+
|
18
|
+
gem.add_development_dependency 'yard'
|
19
|
+
gem.add_development_dependency 'redcarpet'
|
20
|
+
gem.add_dependency 'ruby_protobuf', '~> 0.4.11'
|
21
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require "google_anymote/version"
|
2
|
+
require 'socket'
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
##
|
6
|
+
# Module that understands the Google Anymote protocol.
|
7
|
+
module GoogleAnymote
|
8
|
+
##
|
9
|
+
# Class to send events to a connected GoogleTV
|
10
|
+
class Pair
|
11
|
+
attr_reader :pair, :cert, :host, :gtv
|
12
|
+
|
13
|
+
##
|
14
|
+
# Initializes the Pair class
|
15
|
+
#
|
16
|
+
# @param [Object] cert SSL certificate for this client
|
17
|
+
# @param [String] host hostname or IP address of the Google TV
|
18
|
+
# @param [String] client_name name of the client your connecting from
|
19
|
+
# @param [String] service_name name of the service (generally 'AnyMote')
|
20
|
+
# @return an instance of Pair
|
21
|
+
def initialize(cert, host, client_name = '', service_name = 'AnyMote')
|
22
|
+
@pair = PairingRequest.new
|
23
|
+
@cert = cert
|
24
|
+
@host = host
|
25
|
+
@pair.client_name = client_name
|
26
|
+
@pair.service_name = service_name
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
##
|
31
|
+
# Start the pairing process
|
32
|
+
#
|
33
|
+
# Once the TV recieves the pairing request it will display a 4 digit number.
|
34
|
+
# This number needs to be feed into the next step in the process, complete_pairing().
|
35
|
+
#
|
36
|
+
def start_pairing
|
37
|
+
@gtv = GoogleAnymote::TV.new(@cert, host, 9551 + 1)
|
38
|
+
|
39
|
+
# Let the TV know that we want to pair with it
|
40
|
+
send_message(pair, OuterMessage::MessageType::MESSAGE_TYPE_PAIRING_REQUEST)
|
41
|
+
|
42
|
+
# Build the options and send them to the TV
|
43
|
+
options = Options.new
|
44
|
+
encoding = Options::Encoding.new
|
45
|
+
encoding.type = Options::Encoding::EncodingType::ENCODING_TYPE_HEXADECIMAL
|
46
|
+
encoding.symbol_length = 4
|
47
|
+
options.input_encodings << encoding
|
48
|
+
options.output_encodings << encoding
|
49
|
+
send_message(options, OuterMessage::MessageType::MESSAGE_TYPE_OPTIONS)
|
50
|
+
|
51
|
+
# Build configuration and send it to the TV
|
52
|
+
config = Configuration.new
|
53
|
+
encoding = Options::Encoding.new
|
54
|
+
encoding.type = Options::Encoding::EncodingType::ENCODING_TYPE_HEXADECIMAL
|
55
|
+
config.encoding = encoding
|
56
|
+
config.encoding.symbol_length = 4
|
57
|
+
config.client_role = Options::RoleType::ROLE_TYPE_INPUT
|
58
|
+
outer = send_message(config, OuterMessage::MessageType::MESSAGE_TYPE_CONFIGURATION)
|
59
|
+
|
60
|
+
raise PairingFailed, outer.status unless OuterMessage::Status::STATUS_OK == outer.status
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Complete the pairing process
|
65
|
+
# @param [String] code The code displayed on the Google TV we are trying to pair with.
|
66
|
+
#
|
67
|
+
def complete_pairing(code)
|
68
|
+
# Send secret code to the TV to compete the pairing process
|
69
|
+
secret = Secret.new
|
70
|
+
secret.secret = encode_hex_secret(code)
|
71
|
+
outer = send_message(secret, OuterMessage::MessageType::MESSAGE_TYPE_SECRET)
|
72
|
+
|
73
|
+
# Clean up
|
74
|
+
@gtv.ssl_client.close
|
75
|
+
|
76
|
+
raise PairingFailed, outer.status unless OuterMessage::Status::STATUS_OK == outer.status
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
##
|
84
|
+
# Format and send the message to the GoogleTV
|
85
|
+
#
|
86
|
+
# @param [String] msg message to send
|
87
|
+
# @param [Object] type type of message to send
|
88
|
+
# @return [Object] the OuterMessage response from the TV
|
89
|
+
def send_message(msg, type)
|
90
|
+
# Build the message and get it's size
|
91
|
+
message = wrap_message(msg, type).serialize_to_string
|
92
|
+
message_size = [message.length].pack('N')
|
93
|
+
|
94
|
+
# Write the message to the SSL client and get the response
|
95
|
+
@gtv.ssl_client.write(message_size + message)
|
96
|
+
data = ""
|
97
|
+
@gtv.ssl_client.readpartial(1000,data)
|
98
|
+
@gtv.ssl_client.readpartial(1000,data)
|
99
|
+
|
100
|
+
# Extract the response from the Google TV
|
101
|
+
outer = OuterMessage.new
|
102
|
+
outer.parse_from_string(data)
|
103
|
+
|
104
|
+
return outer
|
105
|
+
end
|
106
|
+
|
107
|
+
##
|
108
|
+
# Wrap the message in an OuterMessage
|
109
|
+
#
|
110
|
+
# @param [String] msg message to send
|
111
|
+
# @param [Object] type type of message to send
|
112
|
+
# @return [Object] a properly formatted OuterMessage
|
113
|
+
def wrap_message(msg, type)
|
114
|
+
# Wrap it in an envelope
|
115
|
+
outer = OuterMessage.new
|
116
|
+
outer.protocol_version = 1
|
117
|
+
outer.status = OuterMessage::Status::STATUS_OK
|
118
|
+
outer.type = type
|
119
|
+
outer.payload = msg.serialize_to_string
|
120
|
+
|
121
|
+
return outer
|
122
|
+
end
|
123
|
+
|
124
|
+
##
|
125
|
+
# Encode the secret from the TV into an OpenSSL Digest
|
126
|
+
#
|
127
|
+
# @param [String] secret pairing code from the TV's screen
|
128
|
+
# @return [Digest] OpenSSL Digest containing the encoded secret
|
129
|
+
def encode_hex_secret secret
|
130
|
+
# TODO(stevenle): Something further encodes the secret to a 64-char hex
|
131
|
+
# string. For now, use adb logcat to figure out what the expected challenge
|
132
|
+
# is. Eventually, make sure the encoding matches the server reference
|
133
|
+
# implementation:
|
134
|
+
# http://code.google.com/p/google-tv-pairing-protocol/source/browse/src/com/google/polo/pairing/PoloChallengeResponse.java
|
135
|
+
|
136
|
+
encoded_secret = [secret.to_i(16)].pack("N").unpack("cccc")[2..3].pack("c*")
|
137
|
+
|
138
|
+
# Per "Polo Implementation Overview", section 6.1, client key material is
|
139
|
+
# hashed first, followed by the server key material, followed by the nonce.
|
140
|
+
digest = OpenSSL::Digest::Digest.new('sha256')
|
141
|
+
digest << @gtv.ssl_client.cert.public_key.n.to_s(2) # client modulus
|
142
|
+
digest << @gtv.ssl_client.cert.public_key.e.to_s(2) # client exponent
|
143
|
+
digest << @gtv.ssl_client.peer_cert.public_key.n.to_s(2) # server modulus
|
144
|
+
digest << @gtv.ssl_client.peer_cert.public_key.e.to_s(2) # server exponent
|
145
|
+
|
146
|
+
digest << encoded_secret[encoded_secret.size / 2]
|
147
|
+
return digest.digest
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require "google_anymote/version"
|
2
|
+
require 'socket'
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
##
|
6
|
+
# Module that understands the Google Anymote protocol.
|
7
|
+
module GoogleAnymote
|
8
|
+
##
|
9
|
+
# Class to send events to a connected GoogleTV
|
10
|
+
class TV
|
11
|
+
attr_reader :host, :port, :cert, :cotext, :ssl_client, :remote, :request, :fling
|
12
|
+
|
13
|
+
##
|
14
|
+
# Initializes the TV class.
|
15
|
+
#
|
16
|
+
# @param [Object] cert SSL certificate for this client
|
17
|
+
# @param [String] host hostname or IP address of the Google TV
|
18
|
+
# @param [Number] port port number of the Google TV
|
19
|
+
# @return an instance of TV
|
20
|
+
def initialize(cert, host, port = 9551)
|
21
|
+
@host = host
|
22
|
+
@port = port
|
23
|
+
@cert = cert
|
24
|
+
@remote = RemoteMessage.new
|
25
|
+
@request = RequestMessage.new
|
26
|
+
@fling = Fling.new
|
27
|
+
|
28
|
+
# Build the SSL stuff
|
29
|
+
@context = OpenSSL::SSL::SSLContext.new
|
30
|
+
@context.key = OpenSSL::PKey::RSA.new @cert
|
31
|
+
@context.cert = OpenSSL::X509::Certificate.new @cert
|
32
|
+
|
33
|
+
connect_to_unit
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# Connect this object to a Google TV
|
38
|
+
def connect_to_unit
|
39
|
+
puts "Connecting to '#{@host}..."
|
40
|
+
begin
|
41
|
+
tcp_client = TCPSocket.new @host, @port
|
42
|
+
@ssl_client = OpenSSL::SSL::SSLSocket.new tcp_client, @context
|
43
|
+
@ssl_client.connect
|
44
|
+
rescue Exception => e
|
45
|
+
puts "Could not connect to '#{@host}: #{e}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# Clean up any sockets or other garbage.
|
51
|
+
def finalize()
|
52
|
+
@ssl_client.close
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Fling a URI to the Google TV connected to this object
|
57
|
+
def fling_uri(uri)
|
58
|
+
@fling.uri = uri
|
59
|
+
@request.fling_message = fling
|
60
|
+
@remote.request_message = @request
|
61
|
+
send_message(@remote)
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
##
|
66
|
+
# Send a message to the Google TV
|
67
|
+
# @param [String] msg message to send to the TV
|
68
|
+
# @return [String] raw data sent back from the TV
|
69
|
+
def send_message(msg)
|
70
|
+
# Build the message and get it's size
|
71
|
+
message = msg.serialize_to_string
|
72
|
+
message_size = [message.length].pack('N')
|
73
|
+
|
74
|
+
# Try to send the message
|
75
|
+
try_again = true
|
76
|
+
begin
|
77
|
+
data = ""
|
78
|
+
@ssl_client.write(message_size + message)
|
79
|
+
@ssl_client.readpartial(1000,data)
|
80
|
+
rescue
|
81
|
+
# Sometimes our connection might drop or something, so
|
82
|
+
# we'll reconnect to the unit and try to send the message again.
|
83
|
+
if try_again
|
84
|
+
try_again = false
|
85
|
+
connect_to_unit
|
86
|
+
retry
|
87
|
+
else
|
88
|
+
# Looks like we couldn't connect to the unit after all.
|
89
|
+
puts "message not sent"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
return data
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|