├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── bin ├── dump-auth-response ├── dump-peer-cert └── inspect-cert ├── index.js ├── lib ├── cast_channel.desc ├── cast_channel.proto ├── channel.js ├── client.js ├── packet-stream-wrapper.js ├── proto.js └── server.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test* 3 | *.pem 4 | *.sig 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Thibaut Séguy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | lib/cast_channel.desc: lib/cast_channel.proto 2 | protoc --descriptor_set_out=$@ --include_imports $< 3 | 4 | private-key.pem: 5 | openssl genrsa -out $@ 1024 6 | 7 | csr.pem: private-key.pem 8 | openssl req -new -key $< -out $@ 9 | 10 | public-cert.pem: private-key.pem csr.pem 11 | openssl x509 -req -in csr.pem -signkey private-key.pem -out $@ 12 | 13 | proto: lib/cast_channel.desc 14 | 15 | tls: private-key.pem public-cert.pem 16 | 17 | clean: 18 | rm lib/cast_channel.desc private-key.pem csr.pem public-cert.pem 19 | 20 | .PHONY: clean proto tls 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | castv2 2 | ====== 3 | 4 | [![NPM version](https://badge.fury.io/js/castv2.svg)](http://badge.fury.io/js/castv2) 5 | [![Dependency Status](https://img.shields.io/david/thibauts/node-castv2.svg)](https://david-dm.org/thibauts/node-castv2) 6 | [![npm](https://img.shields.io/npm/dm/castv2.svg?maxAge=2592000)]() 7 | 8 | ### An implementation of the Chromecast CASTV2 protocol 9 | 10 | This module is an implementation of the Chromecast CASTV2 protocol over TLS. The internet is very sparse on information about the new Chromecast protocol so big props go to [github.com/vincentbernat](https://github.com/vincentbernat) and his [nodecastor](https://github.com/vincentbernat/nodecastor) module that helped me start off on the right foot and save a good deal of time in my research. 11 | 12 | The module provides both a `Client` and a `Server` implementation of the low-level protocol. The server is (sadly) pretty useless because device authentication gets in the way for now (and maybe for good). The client still allows you to connect and exchange messages with a Chromecast dongle without any restriction. 13 | 14 | Installation 15 | ------------ 16 | 17 | ```bash 18 | $ npm install castv2 19 | ``` 20 | 21 | On windows, to avoid native modules dependencies, use 22 | 23 | ```bash 24 | $ npm install castv2 --no-optional 25 | ``` 26 | 27 | Usage 28 | ----- 29 | 30 | ```js 31 | var Client = require('castv2').Client; 32 | var mdns = require('mdns'); 33 | 34 | var browser = mdns.createBrowser(mdns.tcp('googlecast')); 35 | 36 | browser.on('serviceUp', function(service) { 37 | console.log('found device %s at %s:%d', service.name, service.addresses[0], service.port); 38 | ondeviceup(service.addresses[0]); 39 | browser.stop(); 40 | }); 41 | 42 | browser.start(); 43 | 44 | function ondeviceup(host) { 45 | 46 | var client = new Client(); 47 | client.connect(host, function() { 48 | // create various namespace handlers 49 | var connection = client.createChannel('sender-0', 'receiver-0', 'urn:x-cast:com.google.cast.tp.connection', 'JSON'); 50 | var heartbeat = client.createChannel('sender-0', 'receiver-0', 'urn:x-cast:com.google.cast.tp.heartbeat', 'JSON'); 51 | var receiver = client.createChannel('sender-0', 'receiver-0', 'urn:x-cast:com.google.cast.receiver', 'JSON'); 52 | 53 | // establish virtual connection to the receiver 54 | connection.send({ type: 'CONNECT' }); 55 | 56 | // start heartbeating 57 | setInterval(function() { 58 | heartbeat.send({ type: 'PING' }); 59 | }, 5000); 60 | 61 | // launch YouTube app 62 | receiver.send({ type: 'LAUNCH', appId: 'YouTube', requestId: 1 }); 63 | 64 | // display receiver status updates 65 | receiver.on('message', function(data, broadcast) { 66 | if(data.type = 'RECEIVER_STATUS') { 67 | console.log(data.status); 68 | } 69 | }); 70 | }); 71 | 72 | } 73 | ``` 74 | 75 | Run it with the following command to get a full trace of the messages exchanged with the dongle. 76 | 77 | ```bash 78 | $ DEBUG=* node example.js 79 | ``` 80 | 81 | Protocol description 82 | -------------------- 83 | 84 | This is an attempt at documenting the low-level protocol. I hope it will give sender-app makers a clearer picture of what is happening behind the curtain, and give the others ideas about how this kind of protocol can be implemented. The information presented here has been collated from various internet sources (mainly example code and other attempts to implement the protocol) and my own trial and error. Correct me as needed as I may have gotten concepts or namings wrong. 85 | 86 | ### The TLS / Protocol Buffers layer 87 | 88 | The client connects to the Chromecast through TLS on port 8009. Once the connection is established server and client exchange length-prefixed binary messages (that we'll call packets). 89 | 90 | Packets have the following structure : 91 | 92 | ``` 93 | +----------------+------------------------------------------------+ 94 | | Packet length | Payload (message) | 95 | +----------------+------------------------------------------------+ 96 | ``` 97 | 98 | Packet length is a 32 bits Big Endian Unsigned Integer (UInt32BE in nodejs parlance) that determines the payload size. 99 | 100 | Messages are serialized with Protocol Buffers and structured as follows (excerpt of `cast_channel.proto` with comments stripped) : 101 | 102 | ```protobuf 103 | message CastMessage { 104 | enum ProtocolVersion { 105 | CASTV2_1_0 = 0; 106 | } 107 | required ProtocolVersion protocol_version = 1; 108 | 109 | required string source_id = 2; 110 | required string destination_id = 3; 111 | 112 | required string namespace = 4; 113 | 114 | enum PayloadType { 115 | STRING = 0; 116 | BINARY = 1; 117 | } 118 | required PayloadType payload_type = 5; 119 | 120 | optional string payload_utf8 = 6; 121 | optional bytes payload_binary = 7; 122 | } 123 | 124 | ``` 125 | 126 | The original .proto file can also be found in the Chromium source tree. 127 | 128 | Using this structure the sender and receiver *platforms* (eg. The Chrome browser and the Chromecast device) as well as sender and receiver *applications* (eg. a Chromecast receiver app and a Chrome browser sender app for YouTube) communicate on *channels*. 129 | 130 | Senders and receivers identify themselves through IDs : `source_id` and `destination_id`. The sending platform (eg. the Chrome browser) usually uses `sender-0`. The receiving platform (the Chromecast dongle) uses `receiver-0`. Other senders and receivers use identifiers such as `sender-sdqo7ozi6s4a`, `client-4637` or `web-4`. We'll dig into that later. 131 | 132 | ### Namespaces 133 | 134 | Senders and receivers communicate through *channels* defined by the `namespace` field. Each namespace corresponds to a protocol that can have its own semantics. Protocol-specific data is carried in the `payload_utf8` or `payload_binary` fields. Either one or the other is present in the message depending on the `payload_type` field value. Thanks to that, applications can define their own protocols and transparently exchange arbitrary data, including binary data, alleviating the need to establish additional connections (ie. websockets). 135 | 136 | Though, many protocols use JSON encoded messages / commands, which makes them easy to understand and implement. 137 | 138 | Each *sender* or *receiver* can implement one or multiple protocols. For instance the Chromecast *platform* (`receiver-0`) implements the protocols for the following namespaces : `urn:x-cast:com.google.cast.tp.connection`, `urn:x-cast:com.google.cast.tp.heartbeat`, `urn:x-cast:com.google.cast.receiver` and `urn:x-cast:com.google.cast.tp.deviceauth`. 139 | 140 | ### Communicating with receivers 141 | 142 | Before being able to exchange messages with a receiver (be it an *application* or the *platform*), a sender must establish a *virtual connection* with it. This is accomplished through the `urn:x-cast:com.google.cast.tp.connection` namespace / protocol. This has the effect of both allowing the sender to send messages to the receiver, and of subscribing the sender to the receiver's broadcasts (eg. status updates). 143 | 144 | The protocol is JSON encoded and the semantics are pretty simple : 145 | 146 | | **Message payload** | **Description** 147 | |:------------------------|:----------------------------------------------------------------------- 148 | | `{ "type": "CONNECT" }` | establishes a virtual connection between the sender and the receiver 149 | | `{ "type": "CLOSE" }` | closes a virtual connection 150 | 151 | The sender may receive a `CLOSE` message from the receiver that terminates the virtual connection. This sometimes happens in error cases. 152 | 153 | Once the virtual connection is established messages can be exchanged. Broadcasts from the receiver will have a `*` value for the `destination_id` field. 154 | 155 | ### Keeping the connection alive 156 | 157 | Connections are kept alive through the `urn:x-cast:com.google.cast.tp.heartbeat` namespace / protocol. At regular intervals, the sender must send a `PING` message that will get answered by a `PONG`. The protocol is JSON encoded. 158 | 159 | | **Message payload** | **Description** 160 | |:------------------------|:----------------------------------------------------------------------- 161 | | `{ "type": "PING" }` | notifies the other end that we are sill alive 162 | | `{ "type": "PONG" }` | the other end acknowledges that we are 163 | 164 | Failing to do so will lead to connection termination. The default interval seems to be 5 seconds. This protocol allows the Chromecast to detect unresponsive / offline senders much quicker than the TCP keepalive mechanism. 165 | 166 | ### Device authentication 167 | 168 | Device authentication enables a sender to authenticate a Chromecast device. Authenticating the device is purely optional from a sender's perspective, though the official SDK libraries do it to prevent rogue Chromecast devices to communicate with the official sender platforms. Device authentication is taken care of by the `urn:x-cast:com.google.cast.tp.deviceauth` namespace / protocol. 169 | 170 | First, the sender sends a *challenge* message to the platform receiver `receiver-0` which responds by either a *response* message containing a signature, certificate and a variable number of certificate authority certificates that the sent certificate is verified against or an *error* message. These 3 payloads are protocol buffers encoded and described in `cast_channel.proto` as follows : 171 | 172 | ```protobuf 173 | message AuthChallenge { 174 | } 175 | 176 | message AuthResponse { 177 | required bytes signature = 1; 178 | required bytes client_auth_certificate = 2; 179 | repeated bytes client_ca = 3; 180 | } 181 | 182 | message AuthError { 183 | enum ErrorType { 184 | INTERNAL_ERROR = 0; 185 | NO_TLS = 1; // The underlying connection is not TLS 186 | } 187 | required ErrorType error_type = 1; 188 | } 189 | 190 | message DeviceAuthMessage { 191 | optional AuthChallenge challenge = 1; 192 | optional AuthResponse response = 2; 193 | optional AuthError error = 3; 194 | } 195 | ``` 196 | 197 | The challenge message is empty in the current version of the protocol (CAST v2.1.0), yet official sender platforms are checking the returned certificate and signature. Details of the verification process can be found in [this issue](https://github.com/thibauts/node-castv2-messagebus/issues/2). 198 | 199 | ### Controlling applications 200 | 201 | The platform receiver `receiver-0` implements the `urn:x-cast:com.google.cast.receiver` namespace / protocol which provides an interface to *launch*, *stop*, and *query the status* of running applications. `receiver-0` also broadcast status messages on this namespace when other senders launch, stop, or affect the status of running apps. It also allows checking the app for availability. 202 | 203 | The protocol is JSON encoded and is request / response based. Requests include a `type` field containing the type of the request, namely `LAUNCH`, `STOP`, `GET_STATUS` and `GET_APP_AVAILABILITY`, and a `requestId` field that will be reflected in the receiver's response and allows the sender to pair request and responses. `requestId` is not shown in the table below but must be present in every request. In the wild, it is an initially random integer that gets incremented for each subsequent request. 204 | 205 | | **Message payload** | **Description** 206 | |:-----------------------------------------------------|:----------------------------------------------------------------------- 207 | | `{ "type": "LAUNCH", appId: }` | launches an application 208 | | `{ "type": "STOP", sessionId: }` | stops a running instance of an application 209 | | `{ "type": "GET_STATUS" }` | returns the status of the platform receiver, including details about running apps. 210 | | `{ "type": "GET_APP_AVAILABILITY", appId: }` | returns availability of requested apps. `appId` is an array of application IDs. 211 | 212 | `appId` may be eg. `YouTube` or `CC1AD845` for the *Default Media Receiver* app. A `sessionId` identifies a running instance of an application and is provided in status messages. 213 | 214 | As these requests affect the receiver's status they all return a `RECEIVER_STATUS` message of the following form : 215 | 216 | ```json 217 | { 218 | "requestId": 8476438, 219 | "status": { 220 | "applications": [ 221 | { "appId": "CC1AD845", 222 | "displayName": "Default Media Receiver", 223 | "namespaces": [ 224 | "urn:x-cast:com.google.cast.player.message", 225 | "urn:x-cast:com.google.cast.media" 226 | ], 227 | "sessionId": "7E2FF513-CDF6-9A91-2B28-3E3DE7BAC174", 228 | "statusText": "Ready To Cast", 229 | "transportId": "web-5" } 230 | ], 231 | "isActiveInput": true, 232 | "volume": { 233 | "level": 1, 234 | "muted": false 235 | } 236 | }, 237 | "type": "RECEIVER_STATUS" 238 | } 239 | ``` 240 | 241 | This response indicates an instance of the *Default Media Receiver* is running with `sessionId 7E2FF513-CDF6-9A91-2B28-3E3DE7BAC174`. `namespaces` indicates which protocols are supported by the running app. This could allow any *sender application* implementing the *media protocol* to control playback on this session. 242 | 243 | Another important field here is `transportId` as it is the destinationId to be used to communicate with the app. Note that the app being a receiver like any other you must issue it a `CONNECT` message through the `urn:x-cast:com.google.cast.tp.connection` protocol before being able to send messages. In this case, this will have the side effect of subscribing you to media updates (on the media channel) of this *Default Media Player* session. 244 | 245 | You can join an existing session (launched by another sender) by issuing the same `CONNECT` message. 246 | 247 | ### Controlling device volume 248 | 249 | `receiver-0` allows setting volume and muting at the device-level through the `SET_VOLUME` request on `urn:x-cast:com.google.cast.receiver`. 250 | 251 | | **Message payload** | **Description** 252 | |:-----------------------------------------------------------|:----------------------------------------------------------- 253 | | `{ "type": "SET_VOLUME", "volume": { level: } }` | sets volume. `level` is a float between 0 and 1 254 | | `{ "type": "SET_VOLUME", "volume": { muted: } }` | mutes / unmutes. `muted` is true or false 255 | 256 | Contributors 257 | ------------ 258 | 259 | * [jamiees2](https://github.com/jamiees2) (James Sigurðarson) 260 | -------------------------------------------------------------------------------- /bin/dump-auth-response: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var Client = require('../').Client; 3 | var DeviceAuthMessage = require('../').DeviceAuthMessage; 4 | var fs = require('fs'); 5 | var exec = require('child_process').exec; 6 | 7 | var usage = 'Usage: ' + process.argv[0] + ' [port]'; 8 | 9 | var host = process.argv[2]; 10 | var port = process.argv[3] || 8009; 11 | 12 | if(!host) { 13 | console.error(usage) 14 | process.exit(1); 15 | } 16 | 17 | var client = new Client(); 18 | 19 | var opts = { 20 | host: host, 21 | port: port 22 | }; 23 | 24 | client.connect(opts, function() { 25 | var deviceauth = client.createChannel('sender-0', 'receiver-0', 'urn:x-cast:com.google.cast.tp.deviceauth'); 26 | 27 | deviceauth.send(DeviceAuthMessage.serialize({ challenge: {} })); 28 | deviceauth.on('message', function(data, broadcast) { 29 | var response = DeviceAuthMessage.parse(data).response; 30 | client.close(); 31 | 32 | var sigFilename = 'auth-signature.sig'; 33 | var certFilenameDer = 'auth-certificate.der'; 34 | 35 | fs.writeFileSync(sigFilename, response.signature.toString("binary"), "binary"); 36 | fs.writeFileSync(certFilenameDer, response.clientAuthCertificate.toString("binary"), "binary"); 37 | 38 | var certFilenamePem = 'auth-certificate.pem'; 39 | 40 | exec('openssl x509 -in ' + certFilenameDer + ' -inform DER -out ' + certFilenamePem + ' -outform PEM; rm ' + certFilenameDer); 41 | 42 | console.log('output written to %s and %s', sigFilename, certFilenamePem); 43 | 44 | for(var i = 0; i < response.clientCa.length; i++) { 45 | var ca = response.clientCa[i]; 46 | var ca_name = 'auth-ca' + (i + 1) + '.crt'; 47 | 48 | fs.writeFileSync(ca_name, ca.toString("binary"), "binary"); 49 | 50 | console.log('CA written to %s', ca_name); 51 | } 52 | }); 53 | 54 | }); 55 | -------------------------------------------------------------------------------- /bin/dump-peer-cert: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var exec = require('child_process').exec; 3 | var fs = require('fs'); 4 | 5 | var usage = 'Usage: ' + process.argv[0] + ' [port]'; 6 | 7 | var host = process.argv[2]; 8 | var port = process.argv[3] || 8009; 9 | 10 | if(!host) { 11 | console.error(usage) 12 | process.exit(1); 13 | } 14 | 15 | var cmd = 'openssl s_client -connect ' + host + ':' + port + ' < /dev/null'; 16 | 17 | exec(cmd, function(err, stdout, stderr) { 18 | var match = /-----BEGIN CERTIFICATE-----(\s|.)*?-----END CERTIFICATE-----/.exec(stdout); 19 | var cert = match[0]; 20 | var certFilename = 'peer-certificate.pem'; 21 | fs.writeFileSync(certFilename, cert); 22 | console.log('output written to %s', certFilename); 23 | }); 24 | -------------------------------------------------------------------------------- /bin/inspect-cert: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ $# -eq 0 ] 4 | then 5 | echo "Usage: $0 " 6 | exit 1 7 | fi 8 | 9 | openssl x509 -in $1 -text -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Client = require('./lib/client'); 2 | var Server = require('./lib/server'); 3 | var DeviceAuthMessage = require('./lib/proto').DeviceAuthMessage; 4 | 5 | module.exports.Client = Client; 6 | module.exports.Server = Server; 7 | module.exports.DeviceAuthMessage = DeviceAuthMessage; -------------------------------------------------------------------------------- /lib/cast_channel.desc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thibauts/node-castv2/41d589abbf8619ec582e2b2707eb5e3266e7a14a/lib/cast_channel.desc -------------------------------------------------------------------------------- /lib/cast_channel.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | syntax = "proto2"; 6 | 7 | option optimize_for = LITE_RUNTIME; 8 | 9 | package extensions.api.cast_channel; 10 | 11 | message CastMessage { 12 | // Always pass a version of the protocol for future compatibility 13 | // requirements. 14 | enum ProtocolVersion { 15 | CASTV2_1_0 = 0; 16 | } 17 | required ProtocolVersion protocol_version = 1; 18 | 19 | // source and destination ids identify the origin and destination of the 20 | // message. They are used to route messages between endpoints that share a 21 | // device-to-device channel. 22 | // 23 | // For messages between applications: 24 | // - The sender application id is a unique identifier generated on behalf of 25 | // the sender application. 26 | // - The receiver id is always the the session id for the application. 27 | // 28 | // For messages to or from the sender or receiver platform, the special ids 29 | // 'sender-0' and 'receiver-0' can be used. 30 | // 31 | // For messages intended for all endpoints using a given channel, the 32 | // wildcard destination_id '*' can be used. 33 | required string source_id = 2; 34 | required string destination_id = 3; 35 | 36 | // This is the core multiplexing key. All messages are sent on a namespace 37 | // and endpoints sharing a channel listen on one or more namespaces. The 38 | // namespace defines the protocol and semantics of the message. 39 | required string namespace = 4; 40 | 41 | // Encoding and payload info follows. 42 | 43 | // What type of data do we have in this message. 44 | enum PayloadType { 45 | STRING = 0; 46 | BINARY = 1; 47 | } 48 | required PayloadType payload_type = 5; 49 | 50 | // Depending on payload_type, exactly one of the following optional fields 51 | // will always be set. 52 | optional string payload_utf8 = 6; 53 | optional bytes payload_binary = 7; 54 | } 55 | 56 | // Messages for authentication protocol between a sender and a receiver. 57 | message AuthChallenge { 58 | } 59 | 60 | message AuthResponse { 61 | required bytes signature = 1; 62 | required bytes client_auth_certificate = 2; 63 | repeated bytes client_ca = 3; 64 | } 65 | 66 | message AuthError { 67 | enum ErrorType { 68 | INTERNAL_ERROR = 0; 69 | NO_TLS = 1; // The underlying connection is not TLS 70 | } 71 | required ErrorType error_type = 1; 72 | } 73 | 74 | message DeviceAuthMessage { 75 | // Request fields 76 | optional AuthChallenge challenge = 1; 77 | // Response fields 78 | optional AuthResponse response = 2; 79 | optional AuthError error = 3; 80 | } 81 | -------------------------------------------------------------------------------- /lib/channel.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var util = require('util'); 3 | var debug = require('debug')('castv2'); 4 | 5 | function Channel(bus, sourceId, destinationId, namespace, encoding) { 6 | EventEmitter.call(this); 7 | 8 | this.bus = bus; 9 | this.sourceId = sourceId; 10 | this.destinationId = destinationId; 11 | this.namespace = namespace; 12 | this.encoding = encoding; 13 | 14 | var self = this; 15 | 16 | this.bus.on('message', onmessage); 17 | this.once('close', onclose); 18 | 19 | function onmessage(sourceId, destinationId, namespace, data) { 20 | if(sourceId !== self.destinationId) return; 21 | if(destinationId !== self.sourceId && destinationId !== '*') return; 22 | if(namespace !== self.namespace) return; 23 | self.emit('message', decode(data, self.encoding), destinationId === '*'); 24 | } 25 | 26 | function onclose() { 27 | self.bus.removeListener('message', onmessage); 28 | } 29 | } 30 | 31 | util.inherits(Channel, EventEmitter); 32 | 33 | Channel.prototype.send = function(data) { 34 | this.bus.send( 35 | this.sourceId, 36 | this.destinationId, 37 | this.namespace, 38 | encode(data, this.encoding) 39 | ); 40 | }; 41 | 42 | Channel.prototype.close = function() { 43 | this.emit('close'); 44 | }; 45 | 46 | function encode(data, encoding) { 47 | if(!encoding) return data; 48 | switch(encoding) { 49 | case 'JSON': return JSON.stringify(data); 50 | default: throw new Error('Unsupported channel encoding: ' + encoding); 51 | } 52 | } 53 | 54 | function decode(data, encoding) { 55 | if(!encoding) return data; 56 | switch(encoding) { 57 | case 'JSON': return JSON.parse(data); 58 | default: throw new Error('Unsupported channel encoding: ' + encoding); 59 | } 60 | } 61 | 62 | module.exports = Channel; -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var util = require('util'); 3 | var tls = require('tls'); 4 | var debug = require('debug')('castv2'); 5 | var protocol = require('./proto'); 6 | var PacketStreamWrapper = require('./packet-stream-wrapper'); 7 | var Channel = require('./channel'); 8 | 9 | var CastMessage = protocol.CastMessage; 10 | 11 | function Client() { 12 | EventEmitter.call(this); 13 | this.socket = null; 14 | this.ps = null; 15 | } 16 | 17 | util.inherits(Client, EventEmitter); 18 | 19 | Client.prototype.connect = function(options, callback) { 20 | var self = this; 21 | 22 | if(typeof options === 'string') { 23 | options = { 24 | host: options 25 | }; 26 | } 27 | 28 | options.port = options.port || 8009; 29 | options.rejectUnauthorized = false; 30 | 31 | if(callback) this.once('connect', callback); 32 | 33 | debug('connecting to %s:%d ...', options.host, options.port); 34 | 35 | this.socket = tls.connect(options, function() { 36 | self.ps = new PacketStreamWrapper(self.socket); 37 | self.ps.on('packet', onpacket); 38 | 39 | debug('connected'); 40 | self.emit('connect'); 41 | }); 42 | 43 | this.socket.on('error', onerror); 44 | this.socket.once('close', onclose); 45 | 46 | function onerror(err) { 47 | debug('error: %s %j', err.message, err); 48 | self.emit('error', err); 49 | } 50 | 51 | function onclose() { 52 | debug('connection closed'); 53 | self.socket.removeListener('error', onerror); 54 | self.socket = null; 55 | if (self.ps) { 56 | self.ps.removeListener('packet', onpacket); 57 | self.ps = null; 58 | } 59 | self.emit('close'); 60 | } 61 | 62 | function onpacket(buf) { 63 | var message = CastMessage.parse(buf); 64 | 65 | debug( 66 | 'recv message: protocolVersion=%s sourceId=%s destinationId=%s namespace=%s data=%s', 67 | message.protocolVersion, 68 | message.sourceId, 69 | message.destinationId, 70 | message.namespace, 71 | (message.payloadType === 1) // BINARY 72 | ? util.inspect(message.payloadBinary) 73 | : message.payloadUtf8 74 | ); 75 | if(message.protocolVersion !== 0) { // CASTV2_1_0 76 | self.emit('error', new Error('Unsupported protocol version: ' + message.protocolVersion)); 77 | self.close(); 78 | return; 79 | } 80 | 81 | self.emit('message', 82 | message.sourceId, 83 | message.destinationId, 84 | message.namespace, 85 | (message.payloadType === 1) // BINARY 86 | ? message.payloadBinary 87 | : message.payloadUtf8 88 | ); 89 | } 90 | 91 | }; 92 | 93 | Client.prototype.close = function() { 94 | debug('closing connection ...'); 95 | // using socket.destroy here because socket.end caused stalled connection 96 | // in case of dongles going brutally down without a chance to FIN/ACK 97 | this.socket.destroy(); 98 | }; 99 | 100 | Client.prototype.send = function(sourceId, destinationId, namespace, data) { 101 | var message = { 102 | protocolVersion: 0, // CASTV2_1_0 103 | sourceId: sourceId, 104 | destinationId: destinationId, 105 | namespace: namespace 106 | }; 107 | 108 | if(Buffer.isBuffer(data)) { 109 | message.payloadType = 1 // BINARY; 110 | message.payloadBinary = data; 111 | } else { 112 | message.payloadType = 0 // STRING; 113 | message.payloadUtf8 = data; 114 | } 115 | 116 | debug( 117 | 'send message: protocolVersion=%s sourceId=%s destinationId=%s namespace=%s data=%s', 118 | message.protocolVersion, 119 | message.sourceId, 120 | message.destinationId, 121 | message.namespace, 122 | (message.payloadType === 1) // BINARY 123 | ? util.inspect(message.payloadBinary) 124 | : message.payloadUtf8 125 | ); 126 | 127 | var buf = CastMessage.serialize(message); 128 | this.ps.send(buf); 129 | }; 130 | 131 | Client.prototype.createChannel = function(sourceId, destinationId, namespace, encoding) { 132 | return new Channel(this, sourceId, destinationId, namespace, encoding); 133 | }; 134 | 135 | module.exports = Client; 136 | -------------------------------------------------------------------------------- /lib/packet-stream-wrapper.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var inherits = require('util').inherits; 3 | 4 | var WAITING_HEADER = 0; 5 | var WAITING_PACKET = 1; 6 | 7 | function PacketStreamWrapper(stream) { 8 | EventEmitter.call(this); 9 | 10 | this.stream = stream; 11 | 12 | var state = WAITING_HEADER; 13 | var packetLength = 0; 14 | 15 | var self = this; 16 | this.stream.on('readable', function() { 17 | while(true) { 18 | switch(state) { 19 | case WAITING_HEADER: 20 | var header = stream.read(4); 21 | if(header === null) return; 22 | packetLength = header.readUInt32BE(0); 23 | state = WAITING_PACKET; 24 | break; 25 | case WAITING_PACKET: 26 | var packet = stream.read(packetLength); 27 | if(packet === null) return; 28 | self.emit('packet', packet); 29 | state = WAITING_HEADER; 30 | break; 31 | } 32 | } 33 | }); 34 | } 35 | 36 | inherits(PacketStreamWrapper, EventEmitter); 37 | 38 | PacketStreamWrapper.prototype.send = function(buf) { 39 | var header = new Buffer(4); 40 | header.writeUInt32BE(buf.length, 0); 41 | this.stream.write(Buffer.concat([header, buf])); 42 | }; 43 | 44 | module.exports = PacketStreamWrapper; -------------------------------------------------------------------------------- /lib/proto.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var protobuf = require("protobufjs"); 3 | 4 | var builder = protobuf.load(__dirname + "/cast_channel.proto", onLoad); 5 | 6 | var messages = [ 7 | 'CastMessage', 8 | 'AuthChallenge', 9 | 'AuthResponse', 10 | 'AuthError', 11 | 'DeviceAuthMessage' 12 | ]; 13 | 14 | var extensions = []; 15 | 16 | function onLoad(err, root) { 17 | if (err) throw err; 18 | 19 | messages.forEach(function(message) { 20 | extensions[message] = 21 | root.lookupType(`extensions.api.cast_channel.${message}`); 22 | }); 23 | } 24 | 25 | messages.forEach(function(message) { 26 | module.exports[message] = { 27 | serialize: function(data) { 28 | if (!extensions[message]) { 29 | throw new Error('extension not loaded yet'); 30 | } 31 | var Message = extensions[message]; 32 | return Message.encode(data).finish(); 33 | }, 34 | parse: function(data) { 35 | if (!extensions[message]) { 36 | throw new Error('extension not loaded yet'); 37 | } 38 | var Message = extensions[message]; 39 | return Message.decode(data); 40 | } 41 | }; 42 | }); 43 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var util = require('util'); 3 | var tls = require('tls'); 4 | var debug = require('debug')('castv2'); 5 | var protocol = require('./proto'); 6 | var PacketStreamWrapper = require('./packet-stream-wrapper'); 7 | 8 | var CastMessage = protocol.CastMessage; 9 | 10 | function Server(options) { 11 | EventEmitter.call(this); 12 | 13 | this.server = new tls.Server(options); 14 | this.clients = {}; 15 | } 16 | 17 | util.inherits(Server, EventEmitter); 18 | 19 | Server.prototype.listen = function(port, host, callback) { 20 | var self = this; 21 | 22 | var args = Array.prototype.slice.call(arguments); 23 | if(typeof args[args.length - 1] === 'function') { 24 | callback = args.pop(); 25 | } 26 | 27 | this.server.listen.apply(this.server, args.concat([onlisten])); 28 | 29 | this.server.on('secureConnection', onconnect); 30 | this.server.on('error', onerror); 31 | this.server.once('close', onshutdown); 32 | 33 | function onlisten() { 34 | var addr = self.server.address(); 35 | debug('server listening on %s:%d', addr.address, addr.port); 36 | if(callback) callback(); 37 | } 38 | 39 | function onconnect(socket) { 40 | debug('connection from %s:%d', socket.remoteAddress, socket.remotePort); 41 | var ps = new PacketStreamWrapper(socket); 42 | 43 | var clientId = genClientId(socket); 44 | 45 | ps.on('packet', onpacket); 46 | socket.once('close', ondisconnect); 47 | 48 | function onpacket(buf) { 49 | var message = CastMessage.parse(buf); 50 | 51 | debug( 52 | 'recv message: clientId=%s protocolVersion=%s sourceId=%s destinationId=%s namespace=%s data=%s', 53 | clientId, 54 | message.protocolVersion, 55 | message.sourceId, 56 | message.destinationId, 57 | message.namespace, 58 | (message.payloadType === 1) // BINARY 59 | ? util.inspect(message.payloadBinary) 60 | : message.payloadUtf8 61 | ); 62 | 63 | if(message.protocolVersion !== 0) { // CASTV2_1_0 64 | debug('client error: clientId=%s unsupported protocol version (%s)', clientId, message.protocolVersion); 65 | var socket = self.clients[clientId].socket; 66 | socket.end(); 67 | return; 68 | } 69 | 70 | self.emit('message', 71 | clientId, 72 | message.sourceId, 73 | message.destinationId, 74 | message.namespace, 75 | (message.payloadType === 1) // BINARY 76 | ? message.payloadBinary 77 | : message.payloadUtf8 78 | ); 79 | } 80 | 81 | function ondisconnect() { 82 | debug('client %s disconnected', clientId); 83 | ps.removeListener('packet', onpacket); 84 | delete self.clients[clientId]; 85 | } 86 | 87 | self.clients[clientId] = { 88 | socket: socket, 89 | ps: ps 90 | }; 91 | } 92 | 93 | function onshutdown() { 94 | debug('server shutting down'); 95 | self.server.removeListener('secureConnection', onconnect); 96 | self.emit('close'); 97 | } 98 | 99 | function onerror(err) { 100 | debug('error: %s %j', err.message, err); 101 | self.emit('error', err); 102 | } 103 | 104 | }; 105 | 106 | Server.prototype.close = function() { 107 | this.server.close(); 108 | for(var clientId in this.clients) { 109 | var socket = this.clients[clientId].socket; 110 | socket.end(); 111 | } 112 | }; 113 | 114 | Server.prototype.send = function(clientId, sourceId, destinationId, namespace, data) { 115 | var message = { 116 | protocolVersion: 0, // CASTV2_1_0 117 | sourceId: sourceId, 118 | destinationId: destinationId, 119 | namespace: namespace 120 | }; 121 | 122 | if(Buffer.isBuffer(data)) { 123 | message.payloadType = 1 // BINARY; 124 | message.payloadBinary = data; 125 | } else { 126 | message.payloadType = 0 // STRING; 127 | message.payloadUtf8 = data; 128 | } 129 | 130 | debug( 131 | 'send message: clientId=%s protocolVersion=%s sourceId=%s destinationId=%s namespace=%s data=%s', 132 | clientId, 133 | message.protocolVersion, 134 | message.sourceId, 135 | message.destinationId, 136 | message.namespace, 137 | (message.payloadType === 1) // BINARY 138 | ? util.inspect(message.payloadBinary) 139 | : message.payloadUtf8 140 | ); 141 | 142 | var buf = CastMessage.serialize(message); 143 | var ps = this.clients[clientId].ps; 144 | ps.send(buf); 145 | }; 146 | 147 | function genClientId(socket) { 148 | return [socket.remoteAddress, socket.remotePort].join(':'); 149 | } 150 | 151 | module.exports = Server; 152 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "castv2", 3 | "version": "0.1.10", 4 | "description": "An implementation of the Chromecast CASTV2 protocol", 5 | "author": "thibauts", 6 | "license": "MIT", 7 | "main": "index.js", 8 | "dependencies": { 9 | "debug": "^4.1.1", 10 | "protobufjs": "^6.8.8" 11 | }, 12 | "scripts": { 13 | "test": "echo \"Error: no test specified\" && exit 0" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git://github.com/thibauts/node-castv2.git" 18 | }, 19 | "keywords": [ 20 | "chromecast", 21 | "castv2" 22 | ] 23 | } 24 | --------------------------------------------------------------------------------