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 ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.9.3@google_anymote
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in google_anymote.gemspec
4
+ gemspec
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,7 @@
1
+ module GoogleAnymote
2
+ # A general purpose error
3
+ class Error < StandardError; end
4
+
5
+ # Raised when a pairing operation fails for some reason
6
+ class PairingFailed < Error; end
7
+ 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
@@ -0,0 +1,4 @@
1
+ module GoogleAnymote
2
+ # Gem version
3
+ VERSION = "0.0.1"
4
+ end
@@ -0,0 +1,7 @@
1
+ require "google_anymote/version"
2
+ require "google_anymote/tv"
3
+ require "google_anymote/pair"
4
+ require "google_anymote/exceptions"
5
+
6
+ require "proto/remote.pb"
7
+ require "proto/polo.pb"