google_anymote 0.0.1
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 +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
|