├── Makefile ├── .gitignore ├── tests ├── data │ ├── packets │ │ ├── json │ │ ├── gamepad │ │ ├── poweron │ │ ├── disconnect │ │ ├── local_join │ │ ├── poweroff │ │ ├── acknowledge │ │ ├── media_state │ │ ├── connect_request │ │ ├── console_status │ │ ├── gamedvr_record │ │ ├── media_command │ │ ├── connect_response │ │ ├── discovery_request │ │ ├── discovery_response │ │ ├── start_channel_request │ │ └── start_channel_response │ └── selfsigned_cert.crt ├── events.js ├── packet_packer.js ├── xbox.js ├── packet_structure.js ├── sgcrypto.js ├── packet_packer_simple.js └── packet_packer_message.js ├── .codeclimate.yml ├── .github ├── dependabot.yml └── workflows │ ├── auto-merge.yml │ ├── build.yml │ └── publish.yml ├── examples ├── poweron.js ├── poweroff.js ├── discovery.js ├── connect_and_disconnect.js ├── connect.js ├── double_connect.js └── connect_channels.js ├── sonar-project.properties ├── LICENSE ├── package.json ├── src ├── packet │ ├── packer.js │ ├── structure.js │ ├── simple.js │ └── message.js ├── channels │ ├── systemmedia.js │ ├── systeminput.js │ └── tvremote.js ├── channelmanager.js ├── xbox.js ├── sgcrypto.js ├── events.js └── smartglass.js ├── CHANGELOG.md └── README.md /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | npm test 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | coverage 4 | -------------------------------------------------------------------------------- /tests/data/packets/json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-core-node/HEAD/tests/data/packets/json -------------------------------------------------------------------------------- /tests/data/packets/gamepad: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-core-node/HEAD/tests/data/packets/gamepad -------------------------------------------------------------------------------- /tests/data/packets/poweron: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-core-node/HEAD/tests/data/packets/poweron -------------------------------------------------------------------------------- /tests/data/packets/disconnect: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-core-node/HEAD/tests/data/packets/disconnect -------------------------------------------------------------------------------- /tests/data/packets/local_join: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-core-node/HEAD/tests/data/packets/local_join -------------------------------------------------------------------------------- /tests/data/packets/poweroff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-core-node/HEAD/tests/data/packets/poweroff -------------------------------------------------------------------------------- /tests/data/packets/acknowledge: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-core-node/HEAD/tests/data/packets/acknowledge -------------------------------------------------------------------------------- /tests/data/packets/media_state: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-core-node/HEAD/tests/data/packets/media_state -------------------------------------------------------------------------------- /tests/data/packets/connect_request: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-core-node/HEAD/tests/data/packets/connect_request -------------------------------------------------------------------------------- /tests/data/packets/console_status: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-core-node/HEAD/tests/data/packets/console_status -------------------------------------------------------------------------------- /tests/data/packets/gamedvr_record: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-core-node/HEAD/tests/data/packets/gamedvr_record -------------------------------------------------------------------------------- /tests/data/packets/media_command: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-core-node/HEAD/tests/data/packets/media_command -------------------------------------------------------------------------------- /tests/data/packets/connect_response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-core-node/HEAD/tests/data/packets/connect_response -------------------------------------------------------------------------------- /tests/data/packets/discovery_request: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-core-node/HEAD/tests/data/packets/discovery_request -------------------------------------------------------------------------------- /tests/data/packets/discovery_response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-core-node/HEAD/tests/data/packets/discovery_response -------------------------------------------------------------------------------- /tests/data/packets/start_channel_request: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-core-node/HEAD/tests/data/packets/start_channel_request -------------------------------------------------------------------------------- /tests/data/packets/start_channel_response: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenXbox/xbox-smartglass-core-node/HEAD/tests/data/packets/start_channel_response -------------------------------------------------------------------------------- /tests/events.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var fs = require('fs'); 3 | const SGEvents = require('../src/events'); 4 | 5 | describe('smartglassEmitter', function(){ 6 | it('should create a new smartglassEmitter instance', function(){ 7 | var smartglassEmitter = SGEvents() 8 | }); 9 | }) 10 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | eslint: 3 | enabled: true 4 | duplication: 5 | enabled: true 6 | config: 7 | languages: 8 | javascript: 9 | mass_threshold: 65 10 | shellcheck: 11 | enabled: true 12 | checks: 13 | method-count: 14 | enabled: false 15 | ratings: 16 | paths: 17 | - "*.js" 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | # Maintain dependencies for GitHub Actions 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "daily" 9 | 10 | # Maintain dependencies for npm 11 | - package-ecosystem: "npm" 12 | directory: "/" 13 | schedule: 14 | interval: "daily" -------------------------------------------------------------------------------- /examples/poweron.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var Smartglass = require('../src/smartglass'); 3 | 4 | Smartglass().powerOn({ 5 | live_id: 'FD00000000000000', 6 | tries: 5, 7 | ip: '192.168.2.5' 8 | }).then(function(response){ 9 | console.log('Console booted:', response) 10 | }, function(error){ 11 | console.log('Booting console failed:', error) 12 | }); 13 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot Automerge 2 | 3 | on: [pull_request_target] 4 | 5 | jobs: 6 | auto-merge: 7 | runs-on: ubuntu-latest 8 | if: github.actor == 'dependabot[bot]' 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 12 | with: 13 | target: minor 14 | github-token: ${{ secrets.SECRET_GITHUB }} -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.organization=unknownskl-github 2 | sonar.projectKey=xbox-smartglass-core-node 3 | sonar.projectVersion=0.6.10 4 | sonar.sources=src 5 | sonar.tests=tests 6 | sonar.javascript.lcov.reportPaths=coverage/lcov.info 7 | 8 | sonar.links.homepage=https://github.com/unknownskl/xbox-smartglass-core-node 9 | sonar.links.ci=https://github.com/OpenXbox/xbox-smartglass-core-node/actions 10 | sonar.links.scm=https://github.com/unknownskl/xbox-smartglass-core-node 11 | sonar.links.issue=https://github.com/unknownskl/xbox-smartglass-core-node/issues 12 | -------------------------------------------------------------------------------- /examples/poweroff.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var Smartglass = require('../src/smartglass'); 3 | 4 | var sgClient = Smartglass() 5 | 6 | sgClient.connect('192.168.2.5').then(function(){ 7 | console.log('Xbox succesfully connected!'); 8 | 9 | setTimeout(function(){ 10 | sgClient.powerOff().then(function(status){ 11 | console.log('Shutdown succes!') 12 | }, function(error){ 13 | console.log('Shutdown error:', error) 14 | }) 15 | }.bind(sgClient), 1000) 16 | }, function(error){ 17 | console.log(error) 18 | }); 19 | -------------------------------------------------------------------------------- /examples/discovery.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var Smartglass = require('../src/smartglass'); // require('xbox-smartglass-core-node'); 3 | 4 | Smartglass().discovery().then(function(consoles){ 5 | for(var xbox in consoles){ 6 | console.log('- Device found: ' + consoles[xbox].message.name); 7 | console.log(' Address: '+ consoles[xbox].remote.address + ':' + consoles[xbox].remote.port); 8 | } 9 | if(consoles.length == 0){ 10 | console.log('No consoles found on the network') 11 | } 12 | }, function(error){ 13 | console.log(error) 14 | }); 15 | -------------------------------------------------------------------------------- /tests/packet_packer.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var fs = require('fs'); 3 | const Packer = require('../src/packet/packer'); 4 | 5 | describe('packet/packer', function(){ 6 | 7 | it('should return false on the structure if a packet could not be identified', function(){ 8 | var packer_data = Packer('{}') 9 | assert.deepStrictEqual(packer_data.structure, false) 10 | }); 11 | 12 | it('should not return false on the structure if a packet could be identified', function(){ 13 | var packer_data = Packer(Buffer.from('dd01', 'hex')) 14 | assert.notDeepStrictEqual(packer_data.structure, false) 15 | }); 16 | }) 17 | -------------------------------------------------------------------------------- /tests/data/selfsigned_cert.crt: -------------------------------------------------------------------------------- 1 | MIICAzCB7KADAgECAgEBMA0GCSqGSIb3DQEBCwUAMA8xDTALBgNVBAMMBFJ1c3Qw 2 | HhcNMTcwOTMwMTUyNTQyWhcNMTgwOTMwMTUyNTQyWjAWMRQwEgYDVQQDDAtGRkZG 3 | RkZGRkZGRjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABBgV1Tgt95vXkqjYNC+8 4 | cX6s72olj3eSeeVGNXPga/hMaoj6yQSHC/Oib4VuZfSDGVxDI+70egSPI6Ax2mvQ 5 | kp2jLzAtMAsGA1UdDwQEAwIDCDATBgNVHSUEDDAKBggrBgEFBQcDAjAJBgNVHSME 6 | AjAAMA0GCSqGSIb3DQEBCwUAA4IBAQADWPJ8/Pe15pV6r4GCxjeg13vmVHaFm8eU 7 | yc/ZZjDfaQcF2KgJNF0t7Kkvs4v5j9DDLjNLWMt4TxBPHzf6hhrv0Zdqo181i6uO 8 | SluFNg28SdK9uCpZypVXka9hYbdvOLePLUKeCml9k43sFM+NIEr/2oYsTvWg7Bix 9 | CKhb3RCdp3Z3P/8fWtRvTVjuyOpVpO2XPmsbsDQc5IfNeclVr9KE84rxnpC0CI1u 10 | DhuY8iDq20FIBXUY95rljdfcBctgHx/odxtMZdLDRuZTcVgJ/gLxT8h6MgVtYc5i 11 | Kw9MD/iLQ7QpmVr5R0W9KdMKOPOKR3GK4DH5ta40k2GLXjsSTEQO 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: ['push', 'pull_request'] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | 8 | strategy: 9 | matrix: 10 | node: [12.x, 14.x, 16.x] 11 | 12 | name: Node ${{ matrix.node }} 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Setup node 17 | uses: actions/setup-node@v2.5.1 18 | with: 19 | node-version: ${{ matrix.node }} 20 | 21 | - name: Install dependencies 22 | run: npm ci 23 | 24 | - name: Execute tests 25 | run: npm test 26 | 27 | - name: SonarCloud Scan 28 | uses: sonarsource/sonarcloud-github-action@master 29 | if: github.actor != 'dependabot[bot]' 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 UnknownSKL (Jim Kroon) 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/connect_and_disconnect.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var Smartglass = require('../src/smartglass'); 3 | 4 | var deviceStatus = { 5 | current_app: false, 6 | connection_status: false 7 | } 8 | 9 | var sgClient = Smartglass() 10 | sgClient.connect('192.168.2.5').then(function(){ 11 | console.log('Xbox succesfully connected!'); 12 | }, function(error){ 13 | console.log('Failed to connect to xbox:', error); 14 | }); 15 | 16 | sgClient.on('_on_console_status', function(message, xbox, remote, smartglass){ 17 | deviceStatus.connection_status = true 18 | 19 | if(message.packet_decoded.protected_payload.apps[0] != undefined){ 20 | if(deviceStatus.current_app != message.packet_decoded.protected_payload.apps[0].aum_id){ 21 | deviceStatus.current_app = message.packet_decoded.protected_payload.apps[0].aum_id 22 | console.log('Current active app:', deviceStatus) 23 | } 24 | } 25 | }.bind(deviceStatus)); 26 | 27 | sgClient.on('_on_console_status', function(message, xbox, remote, smartglass){ 28 | deviceStatus.connection_status = false 29 | console.log('Sending disconnect to console...') 30 | smartglass.disconnect() 31 | }.bind(deviceStatus)); 32 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: NPM Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | # the Node.js versions to build on 15 | node-version: [14.x] 16 | 17 | steps: 18 | - uses: actions/checkout@master 19 | with: 20 | persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal token 21 | fetch-depth: 0 # otherwise, you will failed to push refs to dest repo 22 | 23 | - name: Get tag name 24 | id: tag_name 25 | run: | 26 | echo ::set-output name=SOURCE_TAG::${GITHUB_REF#refs/tags/} 27 | 28 | - name: Install dependencies 29 | run: npm ci 30 | 31 | - name: Lint the project 32 | run: npm test 33 | 34 | - name: NPM publish beta package 35 | if: ${{ contains(steps.tag_name.outputs.SOURCE_TAG, 'beta') }} 36 | uses: JS-DevTools/npm-publish@v1 37 | with: 38 | token: ${{ secrets.NPM_TOKEN }} 39 | tag: beta 40 | 41 | - name: NPM publish package 42 | if: ${{ !contains(steps.tag_name.outputs.SOURCE_TAG, 'beta') }} 43 | uses: JS-DevTools/npm-publish@v1 44 | with: 45 | token: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xbox-smartglass-core-node", 3 | "version": "0.6.10", 4 | "description": "NodeJS smartglass library for controlling a Xbox", 5 | "main": "src/smartglass.js", 6 | "scripts": { 7 | "test": "nyc --reporter=html --reporter=lcov --reporter=text ./node_modules/mocha/bin/mocha tests/", 8 | "discovery": "DEBUG=smartglass:* ./examples/discovery.js", 9 | "connect": "DEBUG=smartglass:* ./examples/connect.js", 10 | "double_connect": "DEBUG=smartglass:* ./examples/double_connect.js", 11 | "connect_channels": "DEBUG=smartglass:* ./examples/connect_channels.js", 12 | "connect_disconnect": "DEBUG=smartglass:* ./examples/connect_and_disconnect.js", 13 | "poweron": "DEBUG=smartglass:* ./examples/poweron.js", 14 | "poweroff": "DEBUG=smartglass:* ./examples/poweroff.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git@github.com:unknownskl/xbox-smartglass-core-node.git" 19 | }, 20 | "author": "UnknownSKL (Jim Kroon)", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/unknownskl/xbox-smartglass-core-node/issues" 24 | }, 25 | "homepage": "https://github.com/unknownskl/xbox-smartglass-core-node", 26 | "keywords": [ 27 | "xbox", 28 | "xbox one", 29 | "smartglass" 30 | ], 31 | "dependencies": { 32 | "debug": "^4.3.2", 33 | "elliptic": "^6.5.2", 34 | "hex-to-binary": "^1.0.1", 35 | "jsrsasign": "^10.3.0", 36 | "uuid": "^8.3.2", 37 | "uuid-parse": "^1.1.0" 38 | }, 39 | "devDependencies": { 40 | "mocha": "^9.1.3", 41 | "nyc": "^15.1.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/connect.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var Smartglass = require('../src/smartglass'); 3 | 4 | var deviceStatus = { 5 | current_app: false, 6 | connection_status: false, 7 | client: false 8 | } 9 | 10 | deviceStatus.client = Smartglass() 11 | 12 | deviceStatus.client.connect('192.168.2.5').then(function(){ 13 | console.log('Xbox succesfully connected!'); 14 | deviceStatus.connection_status = true 15 | }, function(error){ 16 | console.log('Failed to connect to xbox:', error); 17 | }); 18 | 19 | deviceStatus.client.on('_on_console_status', function(message, xbox, remote, smartglass){ 20 | if(message.packet_decoded.protected_payload.apps[0] != undefined){ 21 | if(deviceStatus.current_app != message.packet_decoded.protected_payload.apps[0].aum_id){ 22 | deviceStatus.current_app = message.packet_decoded.protected_payload.apps[0].aum_id 23 | console.log('xbox: Current active app:', deviceStatus) 24 | } 25 | } 26 | }.bind(deviceStatus)); 27 | 28 | deviceStatus.client.on('_on_timeout', function(message, xbox, remote, smartglass){ 29 | deviceStatus.connection_status = false 30 | console.log('Connection timed out.') 31 | clearInterval(interval) 32 | 33 | deviceStatus.client = Smartglass() 34 | deviceStatus.client.connect('192.168.2.5').then(function(){ 35 | console.log('Xbox succesfully connected!'); 36 | }, function(error){ 37 | console.log('Failed to connect to xbox:', result); 38 | }); 39 | }.bind(deviceStatus, interval)); 40 | 41 | var interval = setInterval(function(){ 42 | console.log('connection_status:', deviceStatus.client._connection_status) 43 | }.bind(deviceStatus), 5000) 44 | -------------------------------------------------------------------------------- /src/packet/packer.js: -------------------------------------------------------------------------------- 1 | var SimplePacket = require('./simple'); 2 | var MessagePacket = require('./message'); 3 | var Debug = require('debug')('smartglass:packer') 4 | 5 | module.exports = function(type) 6 | { 7 | var Types = { 8 | d00d: 'message', 9 | cc00: 'simple.connect_request', 10 | cc01: 'simple.connect_response', 11 | dd00: 'simple.discovery_request', 12 | dd01: 'simple.discovery_response', 13 | dd02: 'simple.poweron', 14 | } 15 | 16 | var loadPacketStructure = function(type, value = false){ 17 | if(type.slice(0, 6) == 'simple'){ 18 | return SimplePacket(type.slice(7), value); 19 | } else if(type.slice(0, 7) == 'message'){ 20 | return MessagePacket(type.slice(8), value); 21 | } else { 22 | Debug('[packet/packer.js] Packet format not found: ', type.toString('hex')); 23 | return false 24 | } 25 | } 26 | 27 | var packet_type = type.slice(0,2).toString('hex') 28 | var structure = '' 29 | 30 | if(packet_type in Types){ 31 | // We got a packet that we need to unpack 32 | var packet_value = type; 33 | type = Types[packet_type]; 34 | structure = loadPacketStructure(type, packet_value) 35 | } else { 36 | structure = loadPacketStructure(type) 37 | } 38 | 39 | return { 40 | type: type, 41 | structure: structure, 42 | set: function(key, value, protected_payload = false){ 43 | this.structure.set(key, value, protected_payload) 44 | }, 45 | pack: function(device = undefined){ 46 | return structure.pack(device) 47 | }, 48 | unpack: function(device = undefined){ 49 | return structure.unpack(device) 50 | }, 51 | setChannel: function(channel){ 52 | this.structure.setChannel(channel) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/double_connect.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var Smartglass = require('../src/smartglass'); 3 | 4 | var deviceStatus = { 5 | current_app: false, 6 | connection_status: false, 7 | clients: [] 8 | } 9 | 10 | var client1 = Smartglass() 11 | var client2 = Smartglass() 12 | 13 | client1.connect('192.168.2.5').then(function(){ 14 | console.log('Xbox succesfully connected!'); 15 | }); 16 | 17 | setTimeout(function(){ 18 | client2.connect('192.168.2.5').then(function(){ 19 | console.log('Xbox succesfully connected!'); 20 | }); 21 | }.bind(deviceStatus), 10000) 22 | 23 | // deviceStatus.clients[0].on('_on_console_status', function(message, xbox, remote, smartglass){ 24 | // deviceStatus.connection_status = true 25 | // 26 | // if(message.packet_decoded.protected_payload.apps[0] != undefined){ 27 | // if(deviceStatus.current_app != message.packet_decoded.protected_payload.apps[0].aum_id){ 28 | // deviceStatus.current_app = message.packet_decoded.protected_payload.apps[0].aum_id 29 | // console.log('Current active app:', deviceStatus) 30 | // } 31 | // } 32 | // }.bind(deviceStatus)); 33 | 34 | // deviceStatus.clients[0].on('_on_timeout', function(message, xbox, remote, smartglass){ 35 | // deviceStatus.connection_status = false 36 | // console.log('Connection timed out.') 37 | // clearInterval(interval) 38 | // 39 | // deviceStatus.client = Smartglass() 40 | // deviceStatus.client.connect({ 41 | // ip: '192.168.2.5' 42 | // }, function(result){ 43 | // if(result === true){ 44 | // console.log('Xbox succesfully connected!'); 45 | // } else { 46 | // console.log('Failed to connect to xbox:', result); 47 | // } 48 | // }); 49 | // }.bind(deviceStatus, interval)); 50 | 51 | var interval = setInterval(function(){ 52 | console.log('connection_status:') 53 | console.log('- 1:', client1._connection_status) 54 | console.log('- 2:', client2._connection_status) 55 | }.bind(deviceStatus), 5000) 56 | -------------------------------------------------------------------------------- /tests/xbox.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var fs = require('fs'); 3 | const Xbox = require('../src/xbox'); 4 | 5 | // var public_key = Buffer.from('041db1e7943878b28c773228ebdcfb05b985be4a386a55f50066231360785f61b60038caf182d712d86c8a28a0e7e2733a0391b1169ef2905e4e21555b432b262d', 'hex'); 6 | var certificate = fs.readFileSync('tests/data/selfsigned_cert.crt') 7 | 8 | describe('xbox', function(){ 9 | it('should create an Xbox object using a public key', function(){ 10 | var xbox = Xbox('127.0.0.1', certificate) 11 | 12 | assert.deepStrictEqual(xbox.getIp(), '127.0.0.1'); 13 | assert.deepStrictEqual(xbox.getCertificate(), certificate); 14 | assert.deepStrictEqual(xbox.getLiveid(), false); 15 | }); 16 | 17 | it('should create a new sgCrypto object using connect()', function(){ 18 | certificate_b64 = certificate.toString().replace(/(\n|\r)+$/, '') 19 | 20 | var xbox = Xbox('127.0.0.1', Buffer.from(certificate_b64, 'base64')) 21 | var connect_request = xbox.connect() 22 | 23 | assert.deepStrictEqual(Buffer.from(connect_request).slice(0, 2), Buffer.from('cc00', 'hex')) 24 | assert.deepStrictEqual(xbox.getLiveid(), 'FFFFFFFFFFF'); 25 | assert.deepStrictEqual(xbox._request_num, 1); 26 | assert.notDeepStrictEqual(xbox._crypto, false); 27 | }); 28 | 29 | it('should create a new sgCrypto object using connect() using credentials', function(){ 30 | certificate_b64 = certificate.toString().replace(/(\n|\r)+$/, '') 31 | 32 | var xbox = Xbox('127.0.0.1', Buffer.from(certificate_b64, 'base64')) 33 | var connect_request = xbox.connect('userhash', 'xsts_token') 34 | 35 | assert.deepStrictEqual(Buffer.from(connect_request).slice(0, 2), Buffer.from('cc00', 'hex')) 36 | assert.deepStrictEqual(xbox.getLiveid(), 'FFFFFFFFFFF'); 37 | assert.deepStrictEqual(xbox._request_num, 1); 38 | assert.notDeepStrictEqual(xbox._crypto, false); 39 | }); 40 | 41 | it('should create an Xbox object and return a value when calling get_requestnum()', function(){ 42 | var xbox = Xbox('127.0.0.1', certificate) 43 | var request_num = xbox.get_requestnum() 44 | 45 | assert.deepStrictEqual(xbox.getIp(), '127.0.0.1'); 46 | assert.deepStrictEqual(xbox.getCertificate(), certificate); 47 | assert.deepStrictEqual(xbox.getLiveid(), false); 48 | 49 | assert.deepStrictEqual(request_num, 1); 50 | 51 | }); 52 | 53 | it('should create an Xbox object and set participant id when using set_participantid()', function(){ 54 | var xbox = Xbox('127.0.0.1', certificate) 55 | xbox.set_participantid(1001) 56 | 57 | assert.deepStrictEqual(xbox.getIp(), '127.0.0.1'); 58 | assert.deepStrictEqual(xbox.getCertificate(), certificate); 59 | assert.deepStrictEqual(xbox.getLiveid(), false); 60 | 61 | assert.deepStrictEqual(xbox._participantid, 1001); 62 | assert.deepStrictEqual(xbox._source_participant_id, 1001); 63 | }); 64 | }) 65 | -------------------------------------------------------------------------------- /src/channels/systemmedia.js: -------------------------------------------------------------------------------- 1 | var Debug = require('debug')('smartglass:channel_system_media') 2 | const Packer = require('../packet/packer'); 3 | const ChannelManager = require('../channelmanager'); 4 | 5 | module.exports = function() 6 | { 7 | var channel_manager = new ChannelManager('48a9ca24eb6d4e128c43d57469edd3cd', 'SystemMedia') 8 | 9 | return { 10 | _channel_manager: channel_manager, 11 | 12 | _media_request_id: 1, 13 | _media_state: { 14 | title_id: 0 15 | }, 16 | 17 | _media_commands: { 18 | play: 2, 19 | pause: 4, 20 | playpause: 8, 21 | stop: 16, 22 | record: 32, 23 | next_track: 64, 24 | prev_track: 128, 25 | fast_forward: 256, 26 | rewind: 512, 27 | channel_up: 1024, 28 | channel_down: 2048, 29 | back: 4096, 30 | view: 8192, 31 | menu: 16384, 32 | seek: 32786, // Not implemented yet 33 | }, 34 | 35 | load: function(smartglass, manager_id){ 36 | this._channel_manager.open(smartglass, manager_id).then(function(channel){ 37 | Debug('Channel is open.') 38 | }, function(error){ 39 | Debug('ChannelManager open() Error:', error) 40 | }) 41 | }, 42 | 43 | sendCommand: function(button){ 44 | return new Promise(function(resolve, reject) { 45 | if(this._channel_manager.getStatus() == true){ 46 | Debug('Send media command: '+button); 47 | 48 | var media_command = Packer('message.media_command') 49 | var request_id = "0000000000000000" 50 | request_id = (request_id+this._media_request_id).slice(-request_id.length); 51 | media_command.set('request_id', Buffer.from(request_id, 'hex')); 52 | media_command.set('title_id', this._media_state.title_id); 53 | media_command.set('command', this._media_commands[button]); 54 | this._media_request_id++ 55 | 56 | media_command.setChannel(this._channel_manager.getChannel()) 57 | this._channel_manager.getConsole().get_requestnum() 58 | var message = media_command.pack(this._channel_manager.getConsole()) 59 | 60 | this._channel_manager.send(message); 61 | 62 | resolve({ 63 | status: 'ok_media_send', 64 | params: { 65 | button: button 66 | } 67 | }) 68 | } else { 69 | reject({ 70 | status: 'error_channel_disconnected', 71 | error: 'Channel not ready: TvRemote' 72 | }) 73 | } 74 | }.bind(this)) 75 | }, 76 | 77 | getState: function(){ 78 | return this._media_state 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.6.10 4 | 5 | - Update dependencies 6 | 7 | ## 0.6.9 8 | 9 | - Fixed typo 'unavalable' to 'unavailable' 10 | - Bump jsrsasign from 8.0.19 to 10.3.0 11 | - Bump lodash from 4.17.20 to 4.17.21 12 | - Bump glob-parent from 5.1.1 to 5.1.2 13 | - Bump uuid from 3.4.0 to 8.3.2 14 | 15 | 16 | ## 0.6.8 17 | 18 | - Bump elliptic from 6.5.3 to 6.5.4 19 | - Bump Bump y18n from 4.0.0 to 4.0.1 20 | 21 | ## 0.6.7 22 | 23 | - Fixed an issue where the library would stop working when received an unknown packet that is not from Smartglass 24 | - Code cleanup 25 | 26 | ## 0.6.6 27 | 28 | - No new package release. CI Changes only 29 | 30 | ## 0.6.5 31 | 32 | - Bump jsrsasign from 8.0.13 to 8.0.19 33 | - Bump lodash from 4.17.15 to 4.17.19 34 | - Bump elliptic from 6.5.2 to 6.5.3 35 | 36 | ## 0.6.4 37 | 38 | - Implemented authentication using a userhash and xsts token 39 | - Added game_dvr_record() function to record the last 60 seconds 40 | 41 | ## 0.6.3 42 | 43 | - Update handlebars to version 4.5.3 44 | 45 | ## 0.6.2 46 | 47 | - Fixed a bug that prevented multiple connections 48 | 49 | ## 0.6.1 50 | 51 | - Added commands to TvRemote channel to get more information about the provider and channel lineups 52 | 53 | ## 0.6.0 54 | 55 | - Smartglass class rewritten to support promises (API breaking changes) 56 | - Performance improvements 57 | 58 | ## 0.5.1 59 | 60 | - Implemented support for JSON fragments (for TvRemote configuration) 61 | - Compatibility for NodeJS 12 by removing the x509 package 62 | 63 | ## 0.5.0 64 | 65 | - Removed cli client from package 66 | - Big update to the API interface of Smartglass (Massive breaking changes) 67 | 68 | ## 0.4.3 69 | 70 | - Added IR commands to control the tv and stb using the xbox (volume, channel, etc..) 71 | 72 | ## 0.4.2 73 | 74 | - Added media control (play/pause) 75 | 76 | ## 0.4.1 77 | 78 | - Added gamepad control 79 | - Media status implemented 80 | - Improved stability in connection 81 | 82 | ## 0.4.0 83 | 84 | - Implemented events in the smartglass client 85 | - Refactored smartglass class 86 | - Booting a device will now check if a boot was successfully 87 | - Fixed docker image 88 | - Improved discovery callback 89 | 90 | ## 0.3.3 91 | 92 | - Improved timeout handling 93 | - Added `_on_timeout()` callback 94 | 95 | ## 0.3.2 96 | 97 | - Fixed discovery on network 98 | - Added disconnect function (See [examples/connect_and_disconnect.js](examples/connect_and_disconnect.js)) 99 | 100 | ## 0.3.1 101 | 102 | - Added debug options using DEBUG=smartglass:* 103 | 104 | ## 0.3.0: 105 | 106 | - Refactored code 107 | - Code coverage using Mocha and SonarQube 108 | - Added examples to connect to the Xbox 109 | 110 | ## 0.2.2: 111 | 112 | - No code changes. Integrated Travis CI + Sonarqube 113 | 114 | ## 0.2.1: 115 | 116 | - Fixed a bug that caused the connection to fail because the path to the python signing component was invalid 117 | 118 | ## 0.2.0: 119 | 120 | - Big update! xbox-smartglass-node-core can connect to the Xbox! For now only polling the status of the active app and tuning off the console 121 | 122 | ## 0.1.3: 123 | 124 | - Fixed a problem where old callbacks were still used when init a new client 125 | -------------------------------------------------------------------------------- /src/channelmanager.js: -------------------------------------------------------------------------------- 1 | const Packer = require('./packet/packer'); 2 | 3 | module.exports = function(service_udid, name) 4 | { 5 | var Debug = require('debug')('smartglass:channelmanager:'+name) 6 | 7 | return { 8 | _channel_status: false, 9 | _channel_name: name, 10 | 11 | _channel_server_id: 0, 12 | _channel_client_id: 0, 13 | _udid: service_udid, 14 | 15 | _smartglass: false, 16 | _xbox: false, 17 | 18 | getStatus: function(){ 19 | return this._channel_status 20 | }, 21 | 22 | getConsole: function(){ 23 | return this._smartglass._console 24 | }, 25 | 26 | getChannel: function(){ 27 | return this._channel_server_id 28 | }, 29 | 30 | getSmartglass: function(){ 31 | return this._smartglass 32 | }, 33 | 34 | open: function(smartglass, channel_id){ 35 | return new Promise(function(resolve, reject) { 36 | Debug('Opening channel #'+channel_id); 37 | 38 | this._channel_client_id = channel_id 39 | this._smartglass = smartglass 40 | 41 | // @TODO: Find a better way to check 42 | this._smartglass.on('_on_console_status', function(message, xbox, remote, client_smartglass){ 43 | if(this._channel_status == false){ 44 | Debug('Request open channel: '+this._channel_name); 45 | 46 | this._xbox = xbox; 47 | 48 | var channel_request = Packer('message.start_channel_request') 49 | channel_request.set('channel_request_id', this._channel_client_id); 50 | channel_request.set('title_id', 0); 51 | channel_request.set('service', Buffer.from(this._udid, 'hex')); 52 | channel_request.set('activity_id', 0); 53 | Debug('+ Send channel request on channel #'+this._channel_client_id); 54 | 55 | // xbox.get_requestnum() 56 | this._smartglass._console.get_requestnum() 57 | var channel_message = channel_request.pack(xbox) 58 | 59 | this._smartglass.on('_on_start_channel_response', function(response_message, response_xbox, response_remote){ 60 | // console.log('Got channel response!', this._channel_client_id, response_message) 61 | 62 | if(response_message.packet_decoded.protected_payload.channel_request_id == this._channel_client_id) 63 | { 64 | if(response_message.packet_decoded.protected_payload.result == 0) 65 | { 66 | Debug('Channel ready: '+this._channel_name); 67 | this._channel_status = true 68 | this._channel_server_id = response_message.packet_decoded.protected_payload.target_channel_id 69 | 70 | resolve(this) 71 | } else { 72 | reject('Could not open channel: '+this._channel_name); 73 | } 74 | } 75 | }.bind(this)); 76 | 77 | this._smartglass._send(channel_message); 78 | } 79 | }.bind(this)) 80 | }.bind(this)) 81 | }, 82 | 83 | send: function(packet){ 84 | this._smartglass._send(packet) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/channels/systeminput.js: -------------------------------------------------------------------------------- 1 | var Debug = require('debug')('smartglass:channel_system_input') 2 | const Packer = require('../packet/packer'); 3 | const ChannelManager = require('../channelmanager'); 4 | 5 | module.exports = function() 6 | { 7 | var channel_manager = new ChannelManager('fa20b8ca66fb46e0adb60b978a59d35f', 'SystemInput') 8 | 9 | return { 10 | _channel_manager: channel_manager, 11 | 12 | _button_map: { 13 | a: 16, 14 | b: 32, 15 | x: 64, 16 | y: 128, 17 | up: 256, 18 | left: 1024, 19 | right: 2048, 20 | down: 512, 21 | nexus: 2, 22 | view: 4, 23 | menu: 8, 24 | }, 25 | 26 | load: function(smartglass, manager_id){ 27 | this._channel_manager.open(smartglass, manager_id).then(function(channel){ 28 | Debug('Channel is open.') 29 | }, function(error){ 30 | Debug('ChannelManager open() Error:', error) 31 | }) 32 | }, 33 | 34 | sendCommand: function(button){ 35 | // Send 36 | return new Promise(function(resolve, reject) { 37 | if(this._channel_manager.getStatus() == true){ 38 | Debug('Send button: '+button); 39 | 40 | if(this._button_map[button] != undefined){ 41 | var timestamp_now = new Date().getTime() 42 | 43 | var gamepad = Packer('message.gamepad') 44 | gamepad.set('timestamp', Buffer.from('000'+timestamp_now.toString(), 'hex')) 45 | gamepad.set('buttons', this._button_map[button]); 46 | gamepad.setChannel(this._channel_manager.getChannel()) 47 | 48 | this._channel_manager.getConsole().get_requestnum() 49 | var message = gamepad.pack(this._channel_manager.getConsole()) 50 | this._channel_manager.send(message); 51 | 52 | setTimeout(function(){ 53 | var timestamp = new Date().getTime() 54 | 55 | var gamepad_unpress = Packer('message.gamepad') 56 | gamepad_unpress.set('timestamp', Buffer.from('000'+timestamp.toString(), 'hex')) 57 | gamepad_unpress.set('buttons', 0); 58 | gamepad_unpress.setChannel(this._channel_manager.getChannel()) 59 | 60 | this._channel_manager.getConsole().get_requestnum() 61 | var message = gamepad_unpress.pack(this._channel_manager.getConsole()) 62 | 63 | this._channel_manager.send(message); 64 | resolve({ 65 | status: 'ok_gamepad_send', 66 | params: { 67 | button: button 68 | } 69 | }) 70 | 71 | }.bind(this), 100) 72 | 73 | } else { 74 | Debug('Failed to send button. Reason: Unknown '+button); 75 | 76 | reject({ 77 | status: 'error_channel_disconnected', 78 | error: 'Channel not ready: SystemInput', 79 | buttons: _button_map 80 | }) 81 | } 82 | } else { 83 | reject({ 84 | status: 'error_channel_disconnected', 85 | error: 'Channel not ready: SystemInput' 86 | }) 87 | } 88 | }.bind(this)) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/packet/structure.js: -------------------------------------------------------------------------------- 1 | module.exports = function(packet) 2 | { 3 | if(packet == undefined) 4 | packet = Buffer.from(''); 5 | 6 | return { 7 | _packet: packet, 8 | _totalLength: packet.length, 9 | _offset: 0, 10 | 11 | setOffset: function(offset) 12 | { 13 | this._offset = offset; 14 | }, 15 | 16 | getOffset: function() 17 | { 18 | return this._offset; 19 | }, 20 | 21 | writeSGString: function(data) 22 | { 23 | var lengthBuffer = Buffer.allocUnsafe(2); 24 | lengthBuffer.writeUInt16BE(data.length, 0); 25 | 26 | var dataBuffer = Buffer.from(data + '\x00'); 27 | 28 | this._add(Buffer.concat([ 29 | lengthBuffer, 30 | dataBuffer 31 | ])); 32 | 33 | return this; 34 | }, 35 | 36 | readSGString: function() 37 | { 38 | var dataLength = this.readUInt16(); 39 | var data = this._packet.slice(this._offset, this._offset+dataLength); 40 | 41 | this._offset = (this._offset+1+dataLength); 42 | 43 | return data; 44 | }, 45 | 46 | writeBytes: function(data, type) 47 | { 48 | var dataBuffer = Buffer.from(data, type); 49 | 50 | this._add(dataBuffer); 51 | return this; 52 | }, 53 | 54 | readBytes: function(count = false) 55 | { 56 | var data = ''; 57 | 58 | if(count == false){ 59 | data = this._packet.slice(this._offset); 60 | this._offset = (this._totalLength); 61 | } else { 62 | data = this._packet.slice(this._offset, this._offset+count); 63 | this._offset = (this._offset+count); 64 | } 65 | 66 | return data; 67 | }, 68 | 69 | writeUInt8: function(data) 70 | { 71 | var tempBuffer = Buffer.allocUnsafe(1); 72 | tempBuffer.writeUInt8(data, 0); 73 | this._add(tempBuffer); 74 | return this; 75 | }, 76 | 77 | readUInt8: function() 78 | { 79 | var data = this._packet.readUInt8(this._offset); 80 | this._offset = (this._offset+1); 81 | 82 | return data; 83 | }, 84 | 85 | writeUInt16: function(data) 86 | { 87 | var tempBuffer = Buffer.allocUnsafe(2); 88 | tempBuffer.writeUInt16BE(data, 0); 89 | this._add(tempBuffer); 90 | return this; 91 | }, 92 | 93 | readUInt16: function() 94 | { 95 | var data = this._packet.readUInt16BE(this._offset); 96 | this._offset = (this._offset+2); 97 | 98 | return data; 99 | }, 100 | 101 | writeUInt32: function(data) 102 | { 103 | var tempBuffer = Buffer.allocUnsafe(4); 104 | tempBuffer.writeUInt32BE(data, 0); 105 | this._add(tempBuffer); 106 | return this; 107 | }, 108 | 109 | readUInt32: function() 110 | { 111 | var data = this._packet.readUInt32BE(this._offset); 112 | this._offset = (this._offset+4); 113 | 114 | return data; 115 | }, 116 | 117 | writeInt32: function(data) 118 | { 119 | var tempBuffer = Buffer.allocUnsafe(4); 120 | tempBuffer.writeInt32BE(data, 0); 121 | this._add(tempBuffer); 122 | return this; 123 | }, 124 | 125 | readInt32: function() 126 | { 127 | var data = this._packet.readInt32BE(this._offset); 128 | this._offset = (this._offset+4); 129 | 130 | return data; 131 | }, 132 | 133 | readUInt64: function() 134 | { 135 | var data = this.readBytes(8) 136 | 137 | return data 138 | }, 139 | 140 | toBuffer: function() 141 | { 142 | return this._packet; 143 | }, 144 | 145 | /* Private functions */ 146 | _add(data) 147 | { 148 | this._packet = Buffer.concat([ 149 | this._packet, 150 | data 151 | ]); 152 | }, 153 | }; 154 | } 155 | -------------------------------------------------------------------------------- /tests/packet_structure.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var PacketStructure = require('../src/packet/structure'); 3 | 4 | describe('packet/structure', function(){ 5 | it('obj._packet should be empty on new instance without parameters', function(){ 6 | var packet = PacketStructure(); 7 | assert.deepStrictEqual(packet.toBuffer(), Buffer.from('')); 8 | }); 9 | it('obj._packet should be not empty on new instance with parameters', function(){ 10 | var packet = PacketStructure(Buffer.from('0x0001')); 11 | assert.deepStrictEqual(packet.toBuffer(), Buffer.from('0x0001')); 12 | }); 13 | 14 | 15 | describe('test write and read types', function(){ 16 | 17 | it('should write UInt8 to the packet and check packet', function(){ 18 | var lPacket = PacketStructure(); 19 | lPacket.writeUInt8(10); 20 | 21 | assert.deepStrictEqual(lPacket.toBuffer(), Buffer.from('\x0a')); 22 | }); 23 | 24 | it('should write UInt16 to the packet and check packet', function(){ 25 | var lPacket = PacketStructure(); 26 | lPacket.writeUInt16(10); 27 | 28 | assert.deepStrictEqual(lPacket.toBuffer(), Buffer.from('\x00\x0a')); 29 | }); 30 | 31 | it('should write UInt32 to the packet and check packet', function(){ 32 | var lPacket = PacketStructure(); 33 | lPacket.writeUInt32(10); 34 | 35 | assert.deepStrictEqual(lPacket.toBuffer(), Buffer.from('\x00\x00\x00\x0a')); 36 | }); 37 | 38 | it('should write SGString to the packet and check packet', function(){ 39 | var lPacket = PacketStructure(); 40 | lPacket.writeSGString('test'); 41 | 42 | assert.deepStrictEqual(lPacket.toBuffer(), Buffer.from('\x00\x04\x74\x65\x73\x74\x00')); 43 | }); 44 | 45 | 46 | 47 | it('should read UInt8 from the packet and check value and offset', function(){ 48 | var lPacket = PacketStructure(); 49 | lPacket.writeUInt8(10); 50 | 51 | var uint16 = lPacket.readUInt8(10); 52 | assert.deepStrictEqual(lPacket.toBuffer(), Buffer.from('\x0a')); 53 | assert.deepStrictEqual(lPacket.getOffset(), 1); 54 | }); 55 | 56 | it('should read UInt16 from the packet and check value and offset', function(){ 57 | var lPacket = PacketStructure(); 58 | lPacket.writeUInt16(10); 59 | 60 | var uint16 = lPacket.readUInt16(10); 61 | assert.deepStrictEqual(lPacket.toBuffer(), Buffer.from('\x00\x0a')); 62 | assert.deepStrictEqual(lPacket.getOffset(), 2); 63 | }); 64 | 65 | it('should read UInt32 from the packet and check value and offset', function(){ 66 | var lPacket = PacketStructure(); 67 | lPacket.writeUInt32(10); 68 | 69 | var uint32 = lPacket.readUInt32(10); 70 | assert.deepStrictEqual(lPacket.toBuffer(), Buffer.from('\x00\x00\x00\x0a')); 71 | assert.deepStrictEqual(lPacket.getOffset(), 4); 72 | }); 73 | 74 | it('should read UInt64 from the packet and check value and offset', function(){ 75 | var lPacket = PacketStructure(Buffer.from('\x67\xa1\x60\x60\x01\x00\x00\x00')); 76 | 77 | var uint64 = lPacket.readUInt64(); // 5911912807 78 | assert.deepStrictEqual(lPacket.toBuffer(), Buffer.from('\x67\xa1\x60\x60\x01\x00\x00\x00')); 79 | // assert.deepStrictEqual(uint64, 5911912807); 80 | assert.deepStrictEqual(lPacket.getOffset(), 8); 81 | }); 82 | 83 | it('should read SGString from the packet and check value and offset', function(){ 84 | var lPacket = PacketStructure(); 85 | lPacket.writeSGString('test'); 86 | 87 | var sgstring = lPacket.readSGString('test'); 88 | assert.deepStrictEqual(lPacket.toBuffer(), Buffer.from('\x00\x04\x74\x65\x73\x74\x00')); 89 | assert.deepStrictEqual(sgstring, Buffer.from('test')); 90 | assert.deepStrictEqual(lPacket.getOffset(), 7); 91 | }); 92 | 93 | 94 | it('should set offset', function(){ 95 | var lPacket = PacketStructure(); 96 | lPacket.writeSGString('testtesttesttest'); 97 | 98 | assert.deepStrictEqual(lPacket.getOffset(), 0); 99 | lPacket.setOffset(7) 100 | assert.deepStrictEqual(lPacket.getOffset(), 7); 101 | lPacket.setOffset(0) 102 | assert.deepStrictEqual(lPacket.getOffset(), 0); 103 | }); 104 | }); 105 | }) 106 | -------------------------------------------------------------------------------- /src/xbox.js: -------------------------------------------------------------------------------- 1 | var Packer = require('./packet/packer'); 2 | const SGCrypto = require('./sgcrypto.js'); 3 | const uuidParse = require('uuid-parse'); 4 | var uuid = require('uuid'); 5 | const os = require('os'); 6 | const EOL = os.EOL; 7 | const crypto = require('crypto'); 8 | var jsrsasign = require('jsrsasign'); 9 | var Debug = require('debug')('smartglass:xbox') 10 | 11 | module.exports = function(ip, certificate) 12 | { 13 | return { 14 | _ip: ip, 15 | _certificate: certificate, 16 | 17 | _iv: false, 18 | _liveid: false, 19 | _is_authenticated: false, 20 | _participantid: false, 21 | 22 | _connection_status: false, 23 | _request_num: 1, 24 | _target_participant_id: 0, 25 | _source_participant_id: 0, 26 | 27 | _fragments: {}, 28 | 29 | _crypto: false, 30 | 31 | getIp: function() 32 | { 33 | return this._ip 34 | }, 35 | 36 | getCertificate: function() 37 | { 38 | return this._certificate 39 | }, 40 | 41 | getLiveid: function() 42 | { 43 | return this._liveid; 44 | }, 45 | 46 | setLiveid: function(liveid) 47 | { 48 | this._liveid = liveid; 49 | }, 50 | 51 | get_requestnum: function() 52 | { 53 | var num = this._request_num; 54 | 55 | this._request_num++; 56 | 57 | Debug('this._request_num set to '+this._request_num) 58 | return num; 59 | }, 60 | 61 | set_participantid: function(participantId) 62 | { 63 | this._participantid = participantId; 64 | this._source_participant_id = participantId; 65 | }, 66 | 67 | connect: function(uhs, xsts_token) 68 | { 69 | // // Set liveid 70 | var pem = '-----BEGIN CERTIFICATE-----'+EOL+this.getCertificate().toString('base64').match(/.{0,64}/g).join('\n')+'-----END CERTIFICATE-----'; 71 | var deviceCert = new jsrsasign.X509(); 72 | deviceCert.readCertPEM(pem); 73 | 74 | // var hSerial = deviceCert.getSerialNumberHex(); // '009e755e" hexadecimal string 75 | // var sIssuer = deviceCert.getIssuerString(); // '/C=US/O=z2' 76 | // var sSubject = deviceCert.getSubjectString(); // '/C=US/O=z2' 77 | // var sNotBefore = deviceCert.getNotBefore(); // '100513235959Z' 78 | // var sNotAfter = deviceCert.getNotAfter(); // '200513235959Z' 79 | 80 | this.setLiveid(deviceCert.getSubjectString().slice(4)) 81 | 82 | // Set uuid 83 | var uuid4 = Buffer.from(uuidParse.parse(uuid.v4())); 84 | 85 | // Create public key 86 | var ecKey = jsrsasign.X509.getPublicKeyFromCertPEM(pem); 87 | 88 | Debug('Signing public key: '+ecKey.pubKeyHex); 89 | 90 | 91 | this._crypto = new SGCrypto(); 92 | var object = this._crypto.signPublicKey(ecKey.pubKeyHex) 93 | 94 | Debug('Crypto output:', object); 95 | 96 | 97 | // Load crypto data 98 | this.loadCrypto(object.public_key, object.secret); 99 | 100 | Debug('Sending connect_request to xbox'); 101 | var discovery_request = Packer('simple.connect_request'); 102 | discovery_request.set('uuid', uuid4); 103 | discovery_request.set('public_key', this._crypto.getPublicKey()); 104 | discovery_request.set('iv', this._crypto.getIv()); 105 | 106 | if(uhs != undefined && xsts_token != undefined){ 107 | Debug('- Connecting using token:', uhs+':'+xsts_token); 108 | discovery_request.set('userhash', uhs, true); 109 | discovery_request.set('jwt', xsts_token, true); 110 | 111 | this._is_authenticated = true 112 | } else { 113 | Debug('- Connecting using anonymous login'); 114 | this._is_authenticated = false 115 | } 116 | 117 | var message = discovery_request.pack(this); 118 | 119 | return message 120 | }, 121 | 122 | loadCrypto: function(public_key, shared_secret) 123 | { 124 | Debug('Loading crypto:'); 125 | Debug('- Public key:', public_key); 126 | Debug('- Shared secret:', shared_secret); 127 | this._crypto = new SGCrypto(); 128 | this._crypto.load(Buffer.from(public_key, 'hex'), Buffer.from(shared_secret, 'hex')) 129 | } 130 | }; 131 | } 132 | -------------------------------------------------------------------------------- /tests/sgcrypto.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var sgCrypto = require('../src/sgcrypto'); 3 | 4 | describe('sgcrypto', function(){ 5 | it('should not generate random iv without a secret', function(){ 6 | var clientCrypto = sgCrypto(); 7 | var rand_iv = clientCrypto.getIv(); 8 | 9 | assert.deepStrictEqual(rand_iv.length, 0); 10 | }); 11 | 12 | it('should return an encryption key', function(){ 13 | var clientCrypto = sgCrypto(); 14 | clientCrypto.load('pubkey', '0123456789012345678901234567890123456789012345678901234567890123'); 15 | var encryptionKey = clientCrypto.getEncryptionKey(); 16 | 17 | assert.deepStrictEqual(encryptionKey, Buffer.from('0123456789012345')); 18 | }); 19 | 20 | it('should return a secret', function(){ 21 | var clientCrypto = sgCrypto(); 22 | clientCrypto.load('pubkey', '0123456789012345678901234567890123456789012345678901234567890123'); 23 | var secret = clientCrypto.getSecret(); 24 | 25 | assert.deepStrictEqual(secret, Buffer.from('0123456789012345678901234567890123456789012345678901234567890123')); 26 | }); 27 | 28 | it('should return a hmac', function(){ 29 | var clientCrypto = sgCrypto(); 30 | clientCrypto.load('pubkey', '0123456789012345678901234567890123456789012345678901234567890123'); 31 | var hmac = clientCrypto.getHmac(); 32 | 33 | assert.deepStrictEqual(hmac, Buffer.from('23456789012345678901234567890123')); 34 | }); 35 | 36 | it('should generate a static iv with a secret', function(){ 37 | var clientCrypto = sgCrypto(); 38 | clientCrypto.load('pubkey', '0123456789012345678901234567890123456789012345678901234567890123'); 39 | 40 | var static_iv = clientCrypto.getIv(); 41 | 42 | assert.deepStrictEqual(static_iv.length, 16); 43 | assert.deepStrictEqual(static_iv, Buffer.from('6789012345678901')); 44 | }); 45 | 46 | it('should generate a static iv with a secret using a seed', function(){ 47 | var clientCrypto = sgCrypto(); 48 | clientCrypto.load('pubkey', '0123456789012345678901234567890123456789012345678901234567890123'); 49 | 50 | var static_iv = clientCrypto.getIv(123456); 51 | 52 | assert.deepStrictEqual(static_iv.length, 16); 53 | assert.deepStrictEqual(static_iv, Buffer.from('6789012345678901')); 54 | }); 55 | 56 | it('should encrypt a string', function(){ 57 | var clientCrypto = sgCrypto(); 58 | clientCrypto.load('pubkey', '0123456789012345678901234567890123456789012345678901234567890123'); 59 | 60 | var key = clientCrypto.getIv(); 61 | var encoded_string = clientCrypto._encrypt(Buffer.from('Test String\x00\x00\x00\x00\x00'), key); 62 | assert.deepStrictEqual(key, Buffer.from('6789012345678901')); 63 | assert.deepStrictEqual(encoded_string, Buffer.from('0a558e2b483d9c4ccc24296c9ac8a85d', 'hex')); 64 | }); 65 | 66 | it('should decode an encrypted string', function(){ 67 | var clientCrypto = sgCrypto(); 68 | clientCrypto.load('pubkey', '0123456789012345678901234567890123456789012345678901234567890123'); 69 | 70 | var key = clientCrypto.getIv(); 71 | var encoded_string = clientCrypto._encrypt(Buffer.from('Test String\x00\x00\x00\x00\x00'), key); 72 | var decoded_string = clientCrypto._decrypt(encoded_string, false, key); 73 | assert.deepStrictEqual(decoded_string, Buffer.from('Test String\x00\x00\x00\x00\x00')); 74 | }); 75 | 76 | it('should encrypt a string with iv', function(){ 77 | var clientCrypto = sgCrypto(); 78 | clientCrypto.load('pubkey', '0123456789012345678901234567890123456789012345678901234567890123'); 79 | 80 | var iv = clientCrypto.getIv(); 81 | var key = Buffer.from('000102030405060708090A0B0C0D0E0F', 'hex'); 82 | 83 | var encoded_string = clientCrypto._encrypt(Buffer.from('Test String\x00\x00\x00\x00\x00'), key, iv); 84 | assert.deepStrictEqual(iv, Buffer.from('6789012345678901')); 85 | assert.deepStrictEqual(encoded_string, Buffer.from('ac641fbc44858dbb6869dfeca062f05c', 'hex')); 86 | }); 87 | 88 | it('should decode an encrypted string with iv', function(){ 89 | var clientCrypto = sgCrypto(); 90 | clientCrypto.load('pubkey', '0123456789012345678901234567890123456789012345678901234567890123'); 91 | 92 | var key = clientCrypto.getIv(); 93 | var iv = Buffer.from('000102030405060708090A0B0C0D0E0F', 'hex'); 94 | var encoded_string = clientCrypto._encrypt(Buffer.from('Test String\x00\x00\x00\x00\x00'), key, iv); 95 | var decoded_string = clientCrypto._decrypt(encoded_string, iv, key); 96 | assert.deepStrictEqual(decoded_string, Buffer.from('Test String\x00\x00\x00\x00\x00')); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /src/sgcrypto.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | 3 | module.exports = function() 4 | { 5 | return { 6 | pubkey: Buffer.from('', 'hex'), 7 | secret: Buffer.from('', 'hex'), 8 | encryptionkey: false, 9 | iv: false, 10 | hash_key: false, 11 | 12 | load: function(pubkey, secret) 13 | { 14 | if(pubkey != undefined && secret != undefined) 15 | { 16 | this.pubkey = Buffer.from(pubkey); 17 | this.secret = Buffer.from(secret); 18 | } 19 | 20 | var data = { 21 | 'aes_key': Buffer.from(this.secret.slice(0, 16)), 22 | 'aes_iv': Buffer.from(this.secret.slice(16, 32)), 23 | 'hmac_key': Buffer.from(this.secret.slice(32)) 24 | }; 25 | 26 | this.iv = data.aes_iv; 27 | this.hash_key = data.hmac_key; 28 | this.encryptionkey = data.aes_key; 29 | }, 30 | 31 | getSecret: function() 32 | { 33 | return this.secret; 34 | }, 35 | 36 | getHmac: function() 37 | { 38 | if(this.encryptionkey == false) 39 | this.load(); 40 | 41 | return this.hash_key; 42 | }, 43 | 44 | signPublicKey: function(public_key) 45 | { 46 | var sha512 = crypto.createHash("sha512"); 47 | 48 | var EC = require('elliptic').ec; 49 | var ec = new EC('p256'); 50 | 51 | // Generate keys 52 | var key1 = ec.genKeyPair(); 53 | var key2 = ec.keyFromPublic(public_key, 'hex') 54 | //var public_key_client = key2 55 | 56 | var shared1 = key1.derive(key2.getPublic()); 57 | var derived_secret = Buffer.from(shared1.toString(16), 'hex') 58 | 59 | var public_key_client = key1.getPublic('hex') 60 | 61 | var pre_salt = Buffer.from('d637f1aae2f0418c', 'hex') 62 | var post_salt = Buffer.from('a8f81a574e228ab7', 'hex') 63 | derived_secret = Buffer.from(pre_salt.toString('hex')+derived_secret.toString('hex')+post_salt.toString('hex'), 'hex') 64 | // Hash shared secret 65 | var sha = sha512.update(derived_secret); 66 | derived_secret = sha.digest(); 67 | 68 | return { 69 | public_key: public_key_client.toString('hex').slice(2), 70 | secret: derived_secret.toString('hex') 71 | } 72 | }, 73 | 74 | getPublicKey: function() 75 | { 76 | return this.pubkey; 77 | }, 78 | 79 | getEncryptionKey: function() 80 | { 81 | if(this.encryptionkey == false) 82 | this.load(); 83 | 84 | return this.encryptionkey; 85 | }, 86 | 87 | getIv: function() 88 | { 89 | if(this.iv == false) 90 | this.load(); 91 | 92 | return this.iv; 93 | }, 94 | 95 | getHashKey: function() 96 | { 97 | if(this.hash_key == false) 98 | this.load(); 99 | 100 | return this.hash_key; 101 | }, 102 | 103 | _encrypt(data, key = false, iv = false) 104 | { 105 | data = Buffer.from(data); 106 | 107 | if(iv == false) 108 | iv = Buffer.from('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'); 109 | 110 | if(key == false){ 111 | key = this.getEncryptionKey() 112 | } 113 | 114 | var cipher = crypto.createCipheriv('aes-128-cbc', key, iv); 115 | 116 | cipher.setAutoPadding(false); 117 | var encryptedPayload = cipher.update(data, 'binary', 'binary'); 118 | encryptedPayload += cipher.final('binary'); 119 | 120 | return Buffer.from(encryptedPayload, 'binary'); 121 | }, 122 | 123 | _decrypt(data, iv, key = false) 124 | { 125 | data = this._addPadding(data); 126 | 127 | if(key == false){ 128 | key = this.getEncryptionKey() 129 | } 130 | 131 | if(iv == false) 132 | iv = Buffer.from('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'); 133 | 134 | var cipher = crypto.createDecipheriv('aes-128-cbc', key, iv); 135 | cipher.setAutoPadding(false); 136 | 137 | var decryptedPayload = cipher.update(data, 'binary', 'binary'); 138 | decryptedPayload += cipher.final('binary'); 139 | 140 | 141 | return this._removePadding(Buffer.from(decryptedPayload, 'binary')); 142 | }, 143 | 144 | _sign: function(data) 145 | { 146 | var hashHmac = crypto.createHmac('sha256', this.getHashKey()); 147 | hashHmac.update(data, 'binary', 'binary'); 148 | var protectedPayloadHash = hashHmac.digest('binary'); 149 | 150 | return Buffer.from(protectedPayloadHash, 'binary'); 151 | }, 152 | 153 | _removePadding(payload) 154 | { 155 | var length = Buffer.from(payload.slice(-1)); 156 | length = length.readUInt8(0); 157 | 158 | if(length > 0 && length < 16) 159 | { 160 | return Buffer.from(payload.slice(0, payload.length-length)); 161 | } else { 162 | return payload; 163 | } 164 | }, 165 | 166 | _addPadding(payload) 167 | { 168 | return payload; 169 | } 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xbox-Smartglass-Core-Node 2 | 3 | [![GitHub Workflow - Build](https://github.com/OpenXbox/xbox-smartglass-core-node/actions/workflows/build.yml/badge.svg?branch=0.6.10)](https://github.com/OpenXbox/xbox-smartglass-core-node/actions) 4 | [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=xbox-smartglass-core-node&metric=alert_status&branch=release/0.6.10)](https://sonarcloud.io/component_measures?id=xbox-smartglass-core-node&metric=alert_status) 5 | [![Technical debt](https://sonarcloud.io/api/project_badges/measure?project=xbox-smartglass-core-node&metric=sqale_index&branch=release/0.6.10)](https://sonarcloud.io/component_measures?id=xbox-smartglass-core-node&metric=sqale_index) 6 | [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=xbox-smartglass-core-node&metric=bugs&branch=release/0.6.10)](https://sonarcloud.io/component_measures?id=xbox-smartglass-core-node&metric=bugs) 7 | [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=xbox-smartglass-core-node&metric=coverage&branch=release/0.6.10)](https://sonarcloud.io/component_measures?id=xbox-smartglass-core-node&metric=coverage) 8 | [![Discord](https://img.shields.io/badge/discord-OpenXbox-blue.svg)](https://openxbox.org/discord) 9 | 10 | 11 | NodeJS smartglass library for controlling a Xbox. 12 | 13 | ## Features 14 | 15 | - Detect Xbox consoles on the network that are turned on 16 | - Retrieve ip address, name and console id 17 | - Simple controls like navigation and controller buttons 18 | - Retrieve active application in realtime 19 | - Send STB and AVR remote commands like channels and volume 20 | 21 | ## Dependencies 22 | 23 | - NodeJS >= 12.x 24 | 25 | ## How to install 26 | 27 | `npm install xbox-smartglass-core-node --save` 28 | 29 | ## Functions 30 | 31 | const Smartglass = require('xbox-smartglass-core-node') 32 | var SystemInputChannel = require('xbox-smartglass-core-node/src/channels/systeminput') 33 | var SystemMediaChannel = require('xbox-smartglass-core-node/src/channels/systemmedia') 34 | var TvRemoteChannel = require('xbox-smartglass-core-node/src/channels/tvremote') 35 | 36 | var sgClient = Smartglass() 37 | sgClient.connect(ip).then(function(){ 38 | sgClient.addManager('system_input', SystemInputChannel()) 39 | sgClient.addManager('system_media', SystemMediaChannel()) 40 | sgClient.addManager('tv_remote', TvRemoteChannel()) 41 | }, function(error){ 42 | console.log(error) 43 | }) 44 | 45 | # Send DVR Record command 46 | 47 | sgClient.recordGameDvr().then(function(status){ 48 | console.log('DVR record send') 49 | }).catch(function(error){ 50 | console.log('DVR record error:', error) 51 | }) 52 | 53 | #### SystemInputChannel 54 | 55 | const Smartglass = require('xbox-smartglass-core-node') 56 | var SystemInputChannel = require('xbox-smartglass-core-node/src/channels/systeminput'); 57 | 58 | var sgClient = Smartglass() 59 | sgClient.addManager('system_input', SystemInputChannel()) 60 | 61 | sgClient.getManager('system_input').sendCommand('nexus').then(function(button){ console.log(button) }, function(error){ console.log(error) }); 62 | sgClient.getManager('system_input').sendCommand('left').then(function(button){ console.log(button) }, function(error){ console.log(error) }); 63 | sgClient.getManager('system_input').sendCommand('a').then(function(button){ console.log(button) }, function(error){ console.log(error) }); 64 | 65 | #### SystemMediaChannel 66 | 67 | const Smartglass = require('xbox-smartglass-core-node') 68 | var SystemMediaChannel = require('xbox-smartglass-core-node/src/channels/systemmedia'); 69 | 70 | var sgClient = Smartglass() 71 | sgClient.addManager('system_media', SystemMediaChannel()) 72 | 73 | sgClient.getManager('system_media').sendCommand('play').then(function(button){ console.log(button) }, function(error){ console.log(error) }); 74 | sgClient.getManager('system_media').sendCommand('pause').then(function(button){ console.log(button) }, function(error){ console.log(error) }); 75 | var media_state = sgClient.getManager('system_media').getState() 76 | 77 | 78 | #### TvRemoteChannel 79 | 80 | const Smartglass = require('xbox-smartglass-core-node') 81 | var TvRemoteChannel = require('xbox-smartglass-core-node/src/channels/tvremote'); 82 | 83 | var sgClient = Smartglass() 84 | sgClient.addManager('tv_remote', TvRemoteChannel()) 85 | 86 | sgClient.getManager('tv_remote').getConfiguration().then(function(configuration){ console.log(configuration) }, function(error){ console.log(error) }); 87 | sgClient.getManager('tv_remote').getHeadendInfo().then(function(configuration){ console.log(configuration) }, function(error){ console.log(error) }); 88 | sgClient.getManager('tv_remote').getLiveTVInfo().then(function(configuration){ console.log(configuration) }, function(error){ console.log(error) }); 89 | sgClient.getManager('tv_remote').getTunerLineups().then(function(configuration){ console.log(configuration) }, function(error){ console.log(error) }); 90 | sgClient.getManager('tv_remote').getAppChannelLineups().then(function(configuration){ console.log(configuration) }, function(error){ console.log(error) }); 91 | 92 | sgClient.getManager('tv_remote').sendIrCommand('btn.vol_up').then(function(button){ console.log(button) }, function(error){ console.log(error) }); 93 | sgClient.getManager('tv_remote').sendIrCommand('btn.vol_down').then(function(button){ console.log(button) }, function(error){ console.log(error) }); 94 | 95 | ## How to use 96 | 97 | See the [examples](examples) folder for examples 98 | 99 | ## Setting up the Xbox 100 | 101 | The plugin needs to be allowed to connect to your Xbox. To allow this make sure you set the setting to allow anonymous connections in Settings -> Devices -> Connections. 102 | 103 | ## Known Issues 104 | 105 | - When sending multiple commands at once the protocol can disconnect 106 | 107 | ## Changelog 108 | 109 | See [changelog](CHANGELOG.md) 110 | -------------------------------------------------------------------------------- /examples/connect_channels.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var Smartglass = require('../src/smartglass'); 3 | var SystemInputChannel = require('../src/channels/systeminput'); 4 | var SystemMediaChannel = require('../src/channels/systemmedia'); 5 | var TvRemoteChannel = require('../src/channels/tvremote'); 6 | 7 | var deviceStatus = { 8 | current_app: false, 9 | connection_status: false, 10 | client: false 11 | } 12 | 13 | deviceStatus.client = Smartglass() 14 | 15 | deviceStatus.client.connect('192.168.2.5').then(function(){ 16 | console.log('Xbox succesfully connected!'); 17 | 18 | deviceStatus.client.addManager('system_input', SystemInputChannel()) 19 | deviceStatus.client.addManager('system_media', SystemMediaChannel()) 20 | deviceStatus.client.addManager('tv_remote', TvRemoteChannel()) 21 | 22 | // 23 | // Volume tests 24 | // 25 | 26 | setTimeout(function(){ 27 | deviceStatus.client.getManager('tv_remote').getConfiguration().then(function(configuration){ 28 | console.log('Configuration:', configuration) 29 | }, function(error){ 30 | console.log('Failed to get configuration:', error) 31 | }); 32 | 33 | setTimeout(function(){ 34 | deviceStatus.client.getManager('tv_remote').sendIrCommand('btn.vol_up').then(function(response){ 35 | console.log('send button:', response) 36 | }, function(error){ 37 | console.log(error) 38 | }); 39 | }.bind(deviceStatus), 1500) 40 | 41 | setTimeout(function(){ 42 | deviceStatus.client.getManager('tv_remote').sendIrCommand('btn.vol_down').then(function(response){ 43 | console.log('send button:', response) 44 | }, function(error){ 45 | console.log(error) 46 | }); 47 | }.bind(deviceStatus), 2500) 48 | 49 | }.bind(deviceStatus), 1000) 50 | 51 | // 52 | // Input tests 53 | // 54 | 55 | setTimeout(function(){ 56 | // deviceStatus.client.getManager('tv_remote').getConfiguration().then(function(configuration){ 57 | // console.log('Configuration:', configuration) 58 | // }, function(error){ 59 | // console.log('Failed to get configuration:', error) 60 | // }); 61 | 62 | setTimeout(function(){ 63 | deviceStatus.client.getManager('system_input').sendCommand('nexus').then(function(){ 64 | // Send button 65 | }, function(error){ 66 | console.log(error) 67 | }); 68 | }.bind(deviceStatus), 1000) 69 | 70 | setTimeout(function(){ 71 | deviceStatus.client.getManager('system_input').sendCommand('nexus').then(function(){ 72 | // Send button 73 | }, function(error){ 74 | console.log(error) 75 | }); 76 | }.bind(deviceStatus), 2000) 77 | 78 | }.bind(deviceStatus), 1000) 79 | 80 | // deviceStatus.client.on('_on_console_status', function(message, xbox, remote){ 81 | // // console.log('CONSOLE STATE', message.packet_decoded.protected_payload) 82 | // // console.log(message.packet_decoded.protected_payload) 83 | // console.log(deviceStatus.client.getActiveApp()) 84 | // //deviceStatus.client.getManager('system_input').sendCommand('nexus'); 85 | // }); 86 | 87 | // setTimeout(function(){ 88 | // console.log('Send nexus button') 89 | // deviceStatus.client.getManager('system_input').sendCommand('nexus'); 90 | // 91 | // setTimeout(function(){ 92 | // deviceStatus.client.getManager('system_input').sendCommand('down'); 93 | // }.bind(deviceStatus), 1000) 94 | // 95 | // setTimeout(function(){ 96 | // deviceStatus.client.getManager('tv_remote').sendIrCommand('btn.vol_up'); 97 | // }.bind(deviceStatus), 1500) 98 | // 99 | // setTimeout(function(){ 100 | // deviceStatus.client.getManager('system_input').sendCommand('up'); 101 | // }.bind(deviceStatus), 2000) 102 | // 103 | // setTimeout(function(){ 104 | // deviceStatus.client.getManager('system_input').sendCommand('left'); 105 | // }.bind(deviceStatus), 3000) 106 | // 107 | // setTimeout(function(){ 108 | // deviceStatus.client.getManager('tv_remote').sendIrCommand('btn.vol_down'); 109 | // }.bind(deviceStatus), 3500) 110 | // 111 | // setTimeout(function(){ 112 | // deviceStatus.client.getManager('system_input').sendCommand('right'); 113 | // }.bind(deviceStatus), 4000) 114 | // 115 | // setTimeout(function(){ 116 | // deviceStatus.client.getManager('system_media').sendCommand('pause'); 117 | // }.bind(deviceStatus), 500) 118 | // 119 | // setTimeout(function(){ 120 | // deviceStatus.client.getManager('system_input').sendCommand('nexus'); 121 | // 122 | // console.log(deviceStatus.client.getActiveApp()) 123 | // console.log(deviceStatus.client.getManager('system_media').getState()) 124 | // }.bind(deviceStatus), 5000) 125 | // 126 | // setTimeout(function(){ 127 | // deviceStatus.client.getManager('system_media').sendCommand('play'); 128 | // }.bind(deviceStatus), 2500) 129 | // 130 | // }.bind(deviceStatus), 5000) 131 | }.bind(deviceStatus), function(error){ 132 | console.log('Failed to connect to xbox:', error); 133 | }); 134 | 135 | deviceStatus.client.on('_on_timeout', function(message, xbox, remote, smartglass){ 136 | deviceStatus.connection_status = false 137 | console.log('Connection timed out.') 138 | clearInterval(interval) 139 | 140 | deviceStatus.client = Smartglass() 141 | deviceStatus.client.connect('192.168.2.5').then(function(){ 142 | console.log('Xbox succesfully connected!'); 143 | }, function(error){ 144 | console.log('Failed to connect to xbox:', error); 145 | }); 146 | }.bind(deviceStatus, interval)); 147 | 148 | var interval = setInterval(function(){ 149 | console.log('connection_status:', deviceStatus.client._connection_status) 150 | }.bind(deviceStatus), 5000) 151 | -------------------------------------------------------------------------------- /tests/packet_packer_simple.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var fs = require('fs'); 3 | const Packer = require('../src/packet/packer'); 4 | var Xbox = require('../src/xbox'); 5 | 6 | var secret = Buffer.from('82bba514e6d19521114940bd65121af2'+'34c53654a8e67add7710b3725db44f77'+'30ed8e3da7015a09fe0f08e9bef3853c0506327eb77c9951769d923d863a2f5e', 'hex'); 7 | var certificate = Buffer.from('041db1e7943878b28c773228ebdcfb05b985be4a386a55f50066231360785f61b60038caf182d712d86c8a28a0e7e2733a0391b1169ef2905e4e21555b432b262d', 'hex'); 8 | 9 | var simple_packets = [ 10 | {'simple.discovery_request': 'tests/data/packets/discovery_request'}, 11 | {'simple.discovery_response': 'tests/data/packets/discovery_response'}, 12 | {'simple.connect_request': 'tests/data/packets/connect_request'}, 13 | // {'simple.connect_response': 'tests/data/packets/connect_response'}, 14 | {'simple.poweron': 'tests/data/packets/poweron'} 15 | ] 16 | 17 | var device = Xbox('127.0.0.1', certificate); 18 | device.loadCrypto(certificate.toString('hex'), secret.toString('hex')); 19 | 20 | describe('packet/packer', function(){ 21 | 22 | it('should unpack a poweron packet', function(){ 23 | var data_packet = fs.readFileSync('tests/data/packets/poweron') 24 | 25 | var poweron_request = Packer(data_packet) 26 | var message = poweron_request.unpack() 27 | 28 | assert.deepStrictEqual(message.type, 'simple') 29 | assert.deepStrictEqual(message.name, 'poweron') 30 | assert.deepStrictEqual(message.packet_decoded.liveid, 'FD00112233FFEE66') 31 | }); 32 | 33 | it('should unpack a discovery_request packet', function(){ 34 | var data_packet = fs.readFileSync('tests/data/packets/discovery_request') 35 | 36 | var discovery_request = Packer(data_packet) 37 | var message = discovery_request.unpack() 38 | 39 | assert.deepStrictEqual(message.type, 'simple') 40 | assert.deepStrictEqual(message.name, 'discovery_request') 41 | assert.deepStrictEqual(message.packet_decoded.flags, 0) 42 | assert.deepStrictEqual(message.packet_decoded.client_type, 8) 43 | assert.deepStrictEqual(message.packet_decoded.min_version, 0) 44 | assert.deepStrictEqual(message.packet_decoded.max_version, 2) 45 | }); 46 | 47 | it('should unpack a discovery_response packet', function(){ 48 | var data_packet = fs.readFileSync('tests/data/packets/discovery_response') 49 | 50 | var discovery_response = Packer(data_packet) 51 | var message = discovery_response.unpack() 52 | 53 | assert.deepStrictEqual(message.type, 'simple') 54 | assert.deepStrictEqual(message.name, 'discovery_response') 55 | assert.deepStrictEqual(message.packet_decoded.flags, 2) 56 | assert.deepStrictEqual(message.packet_decoded.client_type, 1) 57 | assert.deepStrictEqual(message.packet_decoded.name, 'XboxOne') 58 | assert.deepStrictEqual(message.packet_decoded.uuid, 'DE305D54-75B4-431B-ADB2-EB6B9E546014') 59 | assert.deepStrictEqual(message.packet_decoded.last_error, 0) 60 | assert.deepStrictEqual(message.packet_decoded.certificate_length, 519) 61 | assert.deepStrictEqual(message.packet_decoded.certificate.length, 519) 62 | }); 63 | 64 | it('should unpack a connect_request packet', function(){ 65 | var data_packet = fs.readFileSync('tests/data/packets/connect_request') 66 | 67 | var connect_request = Packer(data_packet) 68 | var message = connect_request.unpack(device) 69 | 70 | assert.deepStrictEqual(message.type, 'simple') 71 | assert.deepStrictEqual(message.name, 'connect_request') 72 | assert.deepStrictEqual(message.packet_decoded.payload_length, 98) 73 | assert.deepStrictEqual(message.packet_decoded.protected_payload_length, 47) 74 | assert.deepStrictEqual(message.packet_decoded.uuid, Buffer.from('de305d5475b4431badb2eb6b9e546014', 'hex')) 75 | assert.deepStrictEqual(message.packet_decoded.public_key_type, 0) 76 | //assert.deepStrictEqual(message.packet_decoded.public_key, "\xFF".repeat(64)) 77 | assert.deepStrictEqual(message.packet_decoded.protected_payload.userhash, 'deadbeefdeadbeefde') 78 | assert.deepStrictEqual(message.packet_decoded.protected_payload.jwt, 'dummy_token') 79 | assert.deepStrictEqual(message.packet_decoded.protected_payload.connect_request_num, 0) 80 | assert.deepStrictEqual(message.packet_decoded.protected_payload.connect_request_group_start, 0) 81 | assert.deepStrictEqual(message.packet_decoded.protected_payload.connect_request_group_end, 2) 82 | }); 83 | 84 | it('should unpack a connect_response packet', function(){ 85 | var data_packet = fs.readFileSync('tests/data/packets/connect_response') 86 | 87 | var connect_response = Packer(data_packet) 88 | var message = connect_response.unpack(device) 89 | 90 | assert.deepStrictEqual(message.type, 'simple') 91 | assert.deepStrictEqual(message.name, 'connect_response') 92 | assert.deepStrictEqual(message.packet_decoded.payload_length, 16) 93 | assert.deepStrictEqual(message.packet_decoded.protected_payload_length, 8) 94 | assert.deepStrictEqual(message.packet_decoded.iv, Buffer.from('c6373202bdfd1167cf9693491d22322a', 'hex')) 95 | assert.deepStrictEqual(message.packet_decoded.protected_payload.connect_result, 0) 96 | assert.deepStrictEqual(message.packet_decoded.protected_payload.pairing_state, 0) 97 | assert.deepStrictEqual(message.packet_decoded.protected_payload.participant_id, 31) 98 | }); 99 | 100 | describe('should repack messages correctly', function(){ 101 | simple_packets.forEach(function(element, index){ 102 | for (var name in element) break; 103 | 104 | it('should repack a valid '+name+' packet', function(){ 105 | var data_packet = fs.readFileSync(element[name]) 106 | // console.log('d_packet', data_packet.toString('hex')); 107 | 108 | var response = Packer(data_packet) 109 | var message = response.unpack(device) 110 | //console.log('d_packet message:', message.packet_decoded.decrypted_payload.toString('hex')); 111 | 112 | var repacked = message.pack(device) 113 | // console.log('repacked', repacked.toString('hex')); 114 | 115 | assert.deepStrictEqual(data_packet, Buffer.from(repacked)) 116 | }); 117 | }) 118 | }); 119 | }) 120 | -------------------------------------------------------------------------------- /src/events.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | const Packer = require('./packet/packer') 3 | var Debug = require('debug')('smartglass:events') 4 | 5 | 6 | module.exports = function(){ 7 | const smartglassEmitter = new EventEmitter(); 8 | 9 | smartglassEmitter.on('newListener', function(event, listener){ 10 | Debug('+ New listener: '+event+'()'); 11 | }) 12 | 13 | smartglassEmitter.on('receive', function(message, xbox, remote, smartglass){ 14 | 15 | message = Packer(message); 16 | if(message.structure == false) 17 | return; 18 | 19 | var response = message.unpack(xbox); 20 | 21 | var type = response.name; 22 | var func = ''; 23 | 24 | if(response.packet_decoded.type != 'd00d') 25 | { 26 | func = '_on_' + type.toLowerCase(); 27 | Debug('Received message. Call: '+func+'()'); 28 | } else { 29 | if(response.packet_decoded.target_participant_id != xbox._participantid){ 30 | Debug('[smartglass.js:_receive] Participantid does not match. Ignoring packet.') 31 | return; 32 | } 33 | 34 | func = '_on_' + message.structure.packet_decoded.name.toLowerCase(); 35 | Debug('Received message. Call: '+func+'()'); 36 | 37 | if(response.packet_decoded.flags.need_ack == true){ 38 | Debug('Packet needs to be acknowledged. Sending response'); 39 | //Debug(response.packet_decoded) 40 | 41 | var ack = Packer('message.acknowledge') 42 | ack.set('low_watermark', response.packet_decoded.sequence_number) 43 | // xbox._request_num = response.packet_decoded.sequence_number+1 44 | ack.structure.structure.processed_list.value.push({id: response.packet_decoded.sequence_number}) 45 | smartglass._console.get_requestnum() 46 | var ack_message = ack.pack(smartglass._console) 47 | 48 | try { 49 | smartglass._send(ack_message); 50 | } 51 | catch(error) { 52 | Debug('error', error) 53 | } 54 | 55 | } 56 | } 57 | 58 | if(func == '_on_json') 59 | { 60 | // console.log('ON JSON') 61 | var json_message = JSON.parse(response.packet_decoded.protected_payload.json) 62 | // console.log(json_message); 63 | 64 | // Check if JSON is fragmented 65 | if(json_message.datagram_id != undefined){ 66 | Debug('_on_json is fragmented #'+json_message.datagram_id) 67 | if(xbox._fragments[json_message.datagram_id] == undefined){ 68 | // Prepare buffer for JSON 69 | xbox._fragments[json_message.datagram_id] = { 70 | 71 | getValue: function(){ 72 | var buffer = Buffer.from(''); 73 | 74 | for(let partial in this.partials){ 75 | buffer = Buffer.concat([ 76 | buffer, 77 | Buffer.from(this.partials[partial]) 78 | ]) 79 | } 80 | 81 | var buffer_decoded = Buffer(buffer.toString(), 'base64') 82 | return buffer_decoded 83 | }, 84 | isValid: function(){ 85 | var json = this.getValue() 86 | // console.log('fragment', fragment.toString()) 87 | // var json = Buffer(fragment.toString(), 'base64') 88 | // console.log('valid check: ', json.toString()) 89 | 90 | try { 91 | JSON.parse(json.toString()); 92 | } catch (e) { 93 | return false; 94 | } 95 | 96 | return true 97 | }, 98 | partials: {} 99 | } 100 | } 101 | 102 | xbox._fragments[json_message.datagram_id].partials[json_message.fragment_offset] = json_message.fragment_data 103 | 104 | if(xbox._fragments[json_message.datagram_id].isValid() == true){ 105 | Debug('_on_json: Completed fragmented packet') 106 | var json_response = response 107 | json_response.packet_decoded.protected_payload.json = xbox._fragments[json_message.datagram_id].getValue().toString() 108 | 109 | smartglassEmitter.emit('_on_json', json_response, xbox, remote, smartglass) 110 | 111 | xbox._fragments[json_message.datagram_id] = undefined 112 | } 113 | 114 | func = '_on_json_fragment' 115 | } 116 | } 117 | 118 | Debug('Emit event:', func) 119 | smartglassEmitter.emit(func, response, xbox, remote, smartglass) 120 | }) 121 | 122 | smartglassEmitter.on('_on_discovery_response', function(message, xbox, remote){}); 123 | 124 | smartglassEmitter.on('_on_connect_response', function(message, xbox, remote, smartglass){ 125 | 126 | if(smartglass._connection_status == true){ 127 | Debug('Ignore connect_response packet. Already connected...') 128 | return; 129 | } 130 | 131 | var participantId = message.packet_decoded.protected_payload.participant_id; 132 | xbox.set_participantid(participantId); 133 | 134 | var connectionResult = message.packet_decoded.protected_payload.connect_result; 135 | if(connectionResult == '0') 136 | { 137 | smartglass._connection_status = true; 138 | 139 | var local_join = Packer('message.local_join'); 140 | var join_message = local_join.pack(xbox); 141 | 142 | smartglass._send(join_message); 143 | 144 | smartglass._interval_timeout = setInterval(function(){ 145 | var seconds_ago = (Math.floor(Date.now() / 1000))-this._last_received_time 146 | 147 | if(seconds_ago == 4){ 148 | Debug('Check timeout: Last packet was '+((Math.floor(Date.now() / 1000))-this._last_received_time+' seconds ago')) 149 | 150 | xbox.get_requestnum() 151 | var ack = Packer('message.acknowledge') 152 | ack.set('low_watermark', xbox._request_num) 153 | var ack_message = ack.pack(xbox) 154 | 155 | this._send(ack_message); 156 | } 157 | 158 | if(seconds_ago > 8){ 159 | Debug('Connection timeout after 8 sec. Call: _on_timeout()') 160 | smartglass._events.emit('_on_timeout', message, xbox, remote, this) 161 | 162 | smartglass._closeClient() 163 | return; 164 | } 165 | }.bind(smartglass, message, xbox, remote), 1000) 166 | } 167 | }); 168 | 169 | 170 | smartglassEmitter.on('_on_console_status', function(message, xbox, remote, smartglass){ 171 | if(message.packet_decoded.protected_payload.apps[0] != undefined){ 172 | if(smartglass._current_app != message.packet_decoded.protected_payload.apps[0].aum_id){ 173 | smartglass._current_app = message.packet_decoded.protected_payload.apps[0].aum_id 174 | // console.log('Current active app:', smartglass._current_app) 175 | } 176 | } 177 | }); 178 | 179 | return smartglassEmitter 180 | }; 181 | -------------------------------------------------------------------------------- /src/channels/tvremote.js: -------------------------------------------------------------------------------- 1 | var Debug = require('debug')('smartglass:channel_tv_remote') 2 | const Packer = require('../packet/packer'); 3 | const ChannelManager = require('../channelmanager'); 4 | 5 | module.exports = function() 6 | { 7 | var channel_manager = new ChannelManager('d451e3b360bb4c71b3dbf994b1aca3a7', 'TvRemote') 8 | 9 | return { 10 | _channel_manager: channel_manager, 11 | _message_num: 0, 12 | 13 | _configuration: {}, 14 | _headend_info: {}, 15 | _live_tv: {}, 16 | _tuner_lineups: {}, 17 | _appchannel_lineups: {}, 18 | 19 | load: function(smartglass, manager_id){ 20 | this._channel_manager.open(smartglass, manager_id).then(function(channel){ 21 | Debug('Channel is open.') 22 | }, function(error){ 23 | Debug('ChannelManager open() Error:', error) 24 | }) 25 | 26 | smartglass.on('_on_json', function(message, xbox, remote, client_smartglass){ 27 | var response = JSON.parse(message.packet_decoded.protected_payload.json) 28 | 29 | if(response.response == "Error"){ 30 | console.log('Got Error:', response) 31 | } else { 32 | if(response.response == 'GetConfiguration'){ 33 | Debug('Got TvRemote Configuration') 34 | this._configuration = response.params 35 | 36 | } else if(response.response == 'GetHeadendInfo') { 37 | Debug('Got Headend Configuration') 38 | this._headend_info = response.params 39 | 40 | } else if(response.response == 'GetLiveTVInfo') { 41 | Debug('Got live tv Info') 42 | this._live_tv = response.params 43 | 44 | } else if(response.response == 'GetTunerLineups') { 45 | Debug('Got live tv Info') 46 | this._tuner_lineups = response.params 47 | 48 | } else if(response.response == 'GetAppChannelLineups') { 49 | Debug('Got live tv Info') 50 | this._appchannel_lineups = response.params 51 | } 52 | // else { 53 | // Debug('UNKNOWN JSON RESPONSE:', response) 54 | // } 55 | 56 | } 57 | 58 | }.bind(this)) 59 | }, 60 | 61 | getConfiguration: function(){ 62 | return new Promise(function(resolve, reject) { 63 | if(this._channel_manager.getStatus() == true){ 64 | Debug('Get configuration'); 65 | 66 | this._message_num++ 67 | var msgId = '2ed6c0fd.'+this._message_num; 68 | 69 | var json_request = { 70 | msgid: msgId, 71 | request: "GetConfiguration", 72 | params: null 73 | } 74 | 75 | var json_packet = this._createJsonPacket(json_request); 76 | this._channel_manager.send(json_packet); 77 | 78 | setTimeout(function(){ 79 | resolve(this._configuration) 80 | }.bind(this), 1000) 81 | } else { 82 | reject({ 83 | status: 'error_channel_disconnected', 84 | error: 'Channel not ready: TvRemote' 85 | }) 86 | } 87 | }.bind(this)) 88 | }, 89 | 90 | getHeadendInfo: function(){ 91 | return new Promise(function(resolve, reject) { 92 | if(this._channel_manager.getStatus() == true){ 93 | Debug('Get headend info'); 94 | 95 | this._message_num++ 96 | var msgId = '2ed6c0fd.'+this._message_num; 97 | 98 | var json_request = { 99 | msgid: msgId, 100 | request: "GetHeadendInfo", 101 | params: null 102 | } 103 | 104 | var json_packet = this._createJsonPacket(json_request); 105 | this._channel_manager.send(json_packet); 106 | 107 | setTimeout(function(){ 108 | resolve(this._headend_info) 109 | }.bind(this), 1000) 110 | } else { 111 | reject({ 112 | status: 'error_channel_disconnected', 113 | error: 'Channel not ready: TvRemote' 114 | }) 115 | } 116 | }.bind(this)) 117 | }, 118 | 119 | getLiveTVInfo: function(){ 120 | return new Promise(function(resolve, reject) { 121 | if(this._channel_manager.getStatus() == true){ 122 | Debug('Get live tv info'); 123 | 124 | this._message_num++ 125 | var msgId = '2ed6c0fd.'+this._message_num; 126 | 127 | var json_request = { 128 | msgid: msgId, 129 | request: "GetLiveTVInfo", 130 | params: null 131 | } 132 | 133 | var json_packet = this._createJsonPacket(json_request); 134 | this._channel_manager.send(json_packet); 135 | 136 | setTimeout(function(){ 137 | resolve(this._live_tv) 138 | }.bind(this), 1000) 139 | } else { 140 | reject({ 141 | status: 'error_channel_disconnected', 142 | error: 'Channel not ready: TvRemote' 143 | }) 144 | } 145 | }.bind(this)) 146 | }, 147 | 148 | getTunerLineups: function(){ 149 | return new Promise(function(resolve, reject) { 150 | if(this._channel_manager.getStatus() == true){ 151 | Debug('Get tuner lineups'); 152 | 153 | this._message_num++ 154 | var msgId = '2ed6c0fd.'+this._message_num; 155 | 156 | var json_request = { 157 | msgid: msgId, 158 | request: "GetTunerLineups", 159 | params: null 160 | } 161 | 162 | var json_packet = this._createJsonPacket(json_request); 163 | this._channel_manager.send(json_packet); 164 | 165 | setTimeout(function(){ 166 | resolve(this._tuner_lineups) 167 | }.bind(this), 1000) 168 | } else { 169 | reject({ 170 | status: 'error_channel_disconnected', 171 | error: 'Channel not ready: TvRemote' 172 | }) 173 | } 174 | }.bind(this)) 175 | }, 176 | 177 | getAppChannelLineups: function(){ 178 | return new Promise(function(resolve, reject) { 179 | if(this._channel_manager.getStatus() == true){ 180 | Debug('Get appchannel lineups'); 181 | 182 | this._message_num++ 183 | var msgId = '2ed6c0fd.'+this._message_num; 184 | 185 | var json_request = { 186 | msgid: msgId, 187 | request: "GetAppChannelLineups", 188 | params: null 189 | } 190 | 191 | var json_packet = this._createJsonPacket(json_request); 192 | this._channel_manager.send(json_packet); 193 | 194 | setTimeout(function(){ 195 | resolve(this._appchannel_lineups) 196 | }.bind(this), 1000) 197 | } else { 198 | reject({ 199 | status: 'error_channel_disconnected', 200 | error: 'Channel not ready: TvRemote' 201 | }) 202 | } 203 | }.bind(this)) 204 | }, 205 | 206 | sendIrCommand: function(button_id, device_id = null){ 207 | return new Promise(function(resolve, reject) { 208 | if(this._channel_manager.getStatus() == true){ 209 | Debug('Send button: '+button_id); 210 | 211 | this._message_num++ 212 | var msgId = '2ed6c0fd.'+this._message_num; 213 | 214 | var json_request = { 215 | msgid: msgId, 216 | request:"SendKey", 217 | params: { 218 | button_id: button_id, 219 | device_id: device_id 220 | } 221 | } 222 | 223 | var json_packet = this._createJsonPacket(json_request); 224 | this._channel_manager.send(json_packet); 225 | 226 | resolve({ 227 | status: 'ok_tvremote_send', 228 | params: json_request.params 229 | }) 230 | } else { 231 | reject({ 232 | status: 'error_channel_disconnected', 233 | error: 'Channel not ready: TvRemote' 234 | }) 235 | } 236 | }.bind(this)) 237 | }, 238 | 239 | _createJsonPacket: function(json){ 240 | 241 | var config_request = Packer('message.json') 242 | config_request.set('json', JSON.stringify(json)); 243 | this._channel_manager.getConsole().get_requestnum() 244 | 245 | config_request.setChannel(this._channel_manager.getChannel()) 246 | 247 | return config_request.pack(this._channel_manager.getConsole()) 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/packet/simple.js: -------------------------------------------------------------------------------- 1 | var PacketStructure = require('./structure'); 2 | var Packer = require('./packer'); 3 | var Debug = require('debug')('smartglass:packet_simple') 4 | 5 | module.exports = function(packet_format, packet_data = false){ 6 | var Type = { 7 | uInt32: function(value){ 8 | return { 9 | value: value, 10 | pack: function(packet_structure){ 11 | return packet_structure.writeUInt32(this.value); 12 | }, 13 | unpack: function(packet_structure){ 14 | return packet_structure.readUInt32(); 15 | } 16 | } 17 | }, 18 | uInt16: function(value){ 19 | return { 20 | value: value, 21 | pack: function(packet_structure){ 22 | return packet_structure.writeUInt16(this.value); 23 | }, 24 | unpack: function(packet_structure){ 25 | return packet_structure.readUInt16(); 26 | } 27 | } 28 | }, 29 | bytes: function(length, value){ 30 | return { 31 | value: value, 32 | length: length, 33 | pack: function(packet_structure){ 34 | return packet_structure.writeBytes(this.value); 35 | }, 36 | unpack: function(packet_structure){ 37 | return packet_structure.readBytes(length); 38 | } 39 | } 40 | }, 41 | sgString: function(value){ 42 | return { 43 | value: value, 44 | pack: function(packet_structure){ 45 | return packet_structure.writeSGString(this.value); 46 | }, 47 | unpack: function(packet_structure){ 48 | return packet_structure.readSGString().toString(); 49 | } 50 | } 51 | } 52 | } 53 | 54 | var Packet = { 55 | poweron: { 56 | liveid: Type.sgString(), 57 | }, 58 | discovery_request: { 59 | flags: Type.uInt32('0'), 60 | client_type: Type.uInt16('3'), 61 | min_version: Type.uInt16('0'), 62 | max_version: Type.uInt16('2') 63 | }, 64 | discovery_response: { 65 | flags: Type.uInt32('0'), 66 | client_type: Type.uInt16('0'), 67 | name: Type.sgString(), 68 | uuid: Type.sgString(), 69 | last_error: Type.uInt32('0'), 70 | certificate_length: Type.uInt16('0'), 71 | certificate: Type.bytes(), 72 | }, 73 | connect_request: { 74 | uuid: Type.bytes(16, ''), 75 | public_key_type: Type.uInt16('0'), 76 | public_key: Type.bytes(64, ''), 77 | iv: Type.bytes(16, ''), 78 | protected_payload: Type.bytes() 79 | }, 80 | connect_request_protected: { 81 | userhash: Type.sgString(''), 82 | jwt: Type.sgString(''), 83 | connect_request_num: Type.uInt32('0'), 84 | connect_request_group_start: Type.uInt32('0'), 85 | connect_request_group_end: Type.uInt32('1') 86 | }, 87 | connect_response: { 88 | iv: Type.bytes(16, ''), 89 | protected_payload: Type.bytes() 90 | }, 91 | connect_response_protected: { 92 | connect_result: Type.uInt16('1'), 93 | pairing_state: Type.uInt16('2'), 94 | participant_id: Type.uInt32('0'), 95 | }, 96 | }; 97 | 98 | var structure = Packet[packet_format]; 99 | 100 | // Load protected payload PacketStructure 101 | if(structure.protected_payload != undefined){ 102 | var protected_payload = PacketStructure(); 103 | var protected_structure = Packet[packet_format+'_protected']; 104 | //structure.protected_payload = protected_structure 105 | var structure_protected = protected_structure 106 | } 107 | 108 | return { 109 | type: 'simple', 110 | name: packet_format, 111 | structure: structure, 112 | structure_protected: structure_protected || false, 113 | packet_data: packet_data, 114 | packet_decoded: false, 115 | 116 | set: function(key, value, is_protected = false){ 117 | if(is_protected == false){ 118 | this.structure[key].value = value 119 | 120 | if(this.structure[key].length != undefined) 121 | this.structure[key].length = value.length 122 | } else { 123 | this.structure_protected[key].value = value 124 | 125 | if(this.structure_protected[key].length != undefined) 126 | this.structure_protected[key].length = value.length 127 | } 128 | }, 129 | 130 | unpack: function(device = undefined){ 131 | var payload = PacketStructure(this.packet_data) 132 | 133 | var packet = { 134 | type: payload.readBytes(2).toString('hex'), 135 | payload_length: payload.readUInt16(), 136 | version: payload.readUInt16() 137 | } 138 | 139 | if(packet.version != '0' && packet.version != '2'){ 140 | packet.protected_payload_length = packet.version 141 | packet.version = payload.readUInt16() 142 | } 143 | 144 | for(name in this.structure){ 145 | packet[name] = this.structure[name].unpack(payload) 146 | this.set(name, packet[name]) 147 | } 148 | 149 | if(packet.type == 'dd02'){ 150 | this.name = 'poweron' 151 | } 152 | 153 | Debug('Unpacking message:', this.name); 154 | Debug('payload:', this.packet_data.toString('hex')); 155 | 156 | // Lets decrypt the data when the payload is encrypted 157 | if(packet.protected_payload != undefined){ 158 | 159 | packet.protected_payload = packet.protected_payload.slice(0, -32); 160 | packet.signature = packet.protected_payload.slice(-32) 161 | 162 | var decrypted_payload = device._crypto._decrypt(packet.protected_payload, packet.iv).slice(0, packet.protected_payload_length); 163 | decrypted_payload = PacketStructure(decrypted_payload) 164 | 165 | 166 | var protected_structure = Packet[packet_format+'_protected']; 167 | packet.protected_payload = {} 168 | 169 | for(name in protected_structure){ 170 | packet.protected_payload[name] = protected_structure[name].unpack(decrypted_payload) 171 | this.set('protected_payload', packet.protected_payload) 172 | } 173 | } 174 | 175 | this.packet_decoded = packet; 176 | 177 | return this; 178 | }, 179 | 180 | pack: function(device = false){ 181 | Debug('Packing message:', this.name); 182 | var payload = PacketStructure() 183 | 184 | for(name in this.structure){ 185 | if(name != 'protected_payload'){ 186 | this.structure[name].pack(payload) 187 | 188 | } else { 189 | var protected_structure = this.structure_protected 190 | 191 | for(var name_struct in protected_structure){ 192 | 193 | if(this.structure.protected_payload.value != undefined){ 194 | protected_structure[name_struct].value = this.structure.protected_payload.value[name_struct] 195 | } 196 | 197 | protected_structure[name_struct].pack(protected_payload) 198 | } 199 | 200 | var protected_payload_length = protected_payload.toBuffer().length 201 | 202 | if(protected_payload.toBuffer().length % 16 > 0) 203 | { 204 | var padStart = protected_payload.toBuffer().length % 16; 205 | var padTotal = (16-padStart); 206 | for(var paddingnum = (padStart+1); paddingnum <= 16; paddingnum++) 207 | { 208 | protected_payload.writeUInt8(padTotal); 209 | } 210 | } 211 | 212 | var protected_payload_length_real = protected_payload.toBuffer().length 213 | var encrypted_payload = device._crypto._encrypt(protected_payload.toBuffer(), device._crypto.getEncryptionKey(), this.structure.iv.value); 214 | payload.writeBytes(encrypted_payload) 215 | } 216 | } 217 | 218 | var packet = ''; 219 | 220 | if(this.name == 'poweron'){ 221 | packet = this._pack(Buffer.from('DD02', 'hex'), payload.toBuffer(), '') 222 | 223 | } else if(this.name == 'discovery_request'){ 224 | packet = this._pack(Buffer.from('DD00', 'hex'), payload.toBuffer(), Buffer.from('0000', 'hex')) 225 | 226 | } else if(this.name == 'discovery_response'){ 227 | packet = this._pack(Buffer.from('DD01', 'hex'), payload.toBuffer(), '2') 228 | 229 | } else if(this.name == 'connect_request'){ 230 | 231 | packet = this._pack(Buffer.from('CC00', 'hex'), payload.toBuffer(), Buffer.from('0002', 'hex'), protected_payload_length, protected_payload_length_real) 232 | 233 | // Sign protected payload 234 | var protected_payload_hash = device._crypto._sign(packet); 235 | packet = Buffer.concat([ 236 | packet, 237 | Buffer.from(protected_payload_hash) 238 | ]); 239 | 240 | } else if(this.name == 'connect_response'){ 241 | packet = this._pack(Buffer.from('CC01', 'hex'), payload.toBuffer(), '2') 242 | 243 | // } else if(this.name == 'connect_request_protected'){ 244 | // // Pad packet 245 | // if(payload.toBuffer().length > 16) 246 | // { 247 | // var padStart = payload.toBuffer().length % 16; 248 | // var padTotal = (16-padStart); 249 | // for(var paddingnum = (padStart+1); paddingnum <= 16; paddingnum++) 250 | // { 251 | // payload.writeUInt8(padTotal); 252 | // 253 | // } 254 | // } 255 | // 256 | // var encrypted_payload = device._crypto._encrypt(payload.toBuffer(), device._crypto.getIv()); 257 | // encrypted_payload = PacketStructure(encrypted_payload) 258 | // 259 | // packet = encrypted_payload.toBuffer(); 260 | } else { 261 | packet = payload.toBuffer(); 262 | } 263 | 264 | return packet; 265 | }, 266 | 267 | _pack: function(type, payload, version, protected_payload_length = false, protected_payload_length_real = 0) 268 | { 269 | var payload_length = PacketStructure(); 270 | 271 | if(protected_payload_length !== false) 272 | { 273 | payload_length.writeUInt16(payload.length-protected_payload_length_real); 274 | payload_length = payload_length.toBuffer(); 275 | 276 | var protected_length = PacketStructure(); 277 | protected_length.writeUInt16(protected_payload_length); 278 | protected_length = protected_length.toBuffer(); 279 | 280 | return Buffer.concat([ 281 | type, 282 | payload_length, 283 | protected_length, 284 | version, 285 | payload 286 | ]); 287 | 288 | } else { 289 | payload_length.writeUInt16(payload.length); 290 | payload_length = payload_length.toBuffer(); 291 | 292 | return Buffer.concat([ 293 | type, 294 | payload_length, 295 | Buffer.from('\x00' + String.fromCharCode(version)), 296 | payload 297 | ]); 298 | } 299 | }, 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/smartglass.js: -------------------------------------------------------------------------------- 1 | const dgram = require('dgram'); 2 | const Packer = require('./packet/packer'); 3 | const Xbox = require('./xbox'); 4 | const Events = require('./events'); 5 | 6 | module.exports = function() 7 | { 8 | var id = Math.floor(Math.random() * (999 - 1)) + 1; 9 | var Debug = require('debug')('smartglass:client-'+id) 10 | 11 | var events = Events() 12 | 13 | return { 14 | _client_id: id, 15 | _console: false, 16 | _socket: false, 17 | _events: events, 18 | 19 | _last_received_time: false, 20 | _is_broadcast: false, 21 | _ip: false, 22 | _interval_timeout: false, 23 | 24 | _managers: {}, 25 | _managers_num: 0, 26 | 27 | _connection_status: false, 28 | _current_app: false, 29 | 30 | discovery: function(ip) 31 | { 32 | if(ip == undefined){ 33 | this._ip = '255.255.255.255' 34 | this._is_broadcast = true 35 | } else { 36 | this._ip = ip 37 | } 38 | 39 | return new Promise(function(resolve, reject) { 40 | this._getSocket() 41 | 42 | Debug('['+this._client_id+'] Crafting discovery_request packet'); 43 | var discovery_packet = Packer('simple.discovery_request') 44 | var message = discovery_packet.pack() 45 | 46 | var consoles_found = [] 47 | 48 | this._events.on('_on_discovery_response', function(message, xbox, remote){ 49 | consoles_found.push({ 50 | message: message.packet_decoded, 51 | remote: remote 52 | }) 53 | 54 | if(this._is_broadcast == false){ 55 | Debug('Console found, clear timeout because we query an ip (direct)') 56 | clearTimeout(this._interval_timeout) 57 | resolve(consoles_found) 58 | this._closeClient(); 59 | } 60 | 61 | }.bind(this)); 62 | 63 | this._send(message); 64 | 65 | this._interval_timeout = setTimeout(function(){ 66 | Debug('Discovery timeout after 2 sec (broadcast)') 67 | this._closeClient(); 68 | 69 | resolve(consoles_found) 70 | }.bind(this), 2000); 71 | }.bind(this)) 72 | }, 73 | 74 | getActiveApp: function() 75 | { 76 | return this._current_app 77 | }, 78 | 79 | isConnected: function() 80 | { 81 | return this._connection_status 82 | }, 83 | 84 | powerOn: function(options) 85 | { 86 | return new Promise(function(resolve, reject) { 87 | this._getSocket(); 88 | 89 | if(options.tries == undefined){ 90 | options.tries = 5; 91 | } 92 | 93 | this._ip = options.ip 94 | 95 | var poweron_packet = Packer('simple.poweron') 96 | poweron_packet.set('liveid', options.live_id) 97 | var message = poweron_packet.pack() 98 | 99 | var try_num = 0; 100 | var sendBoot = function(client, callback) 101 | { 102 | client._send(message); 103 | 104 | try_num = try_num+1; 105 | if(try_num <= options.tries) 106 | { 107 | setTimeout(sendBoot, 1000, client); 108 | } else { 109 | client._closeClient(); 110 | 111 | client.discovery(options.ip).then(function(consoles){ 112 | if(consoles.length > 0){ 113 | resolve({ 114 | status: 'success' 115 | }) 116 | } else { 117 | reject({ 118 | status: 'error_discovery', 119 | error: 'Console was not found on network. Probably failed' 120 | }) 121 | } 122 | }, function(error){ 123 | reject({ 124 | status: 'error_discovery', 125 | error: 'Console was not found on network. Probably failed' 126 | }) 127 | }) 128 | } 129 | } 130 | setTimeout(sendBoot, 1000, this); 131 | }.bind(this)) 132 | }, 133 | 134 | powerOff: function() 135 | { 136 | return new Promise(function(resolve, reject) { 137 | if(this.isConnected() == true){ 138 | Debug('['+this._client_id+'] Sending power off command to: '+this._console._liveid) 139 | 140 | this._console.get_requestnum() 141 | var poweroff = Packer('message.power_off'); 142 | poweroff.set('liveid', this._console._liveid) 143 | var message = poweroff.pack(this._console); 144 | 145 | this._send(message); 146 | 147 | setTimeout(function(){ 148 | this.disconnect() 149 | resolve(true) 150 | }.bind(this), 1000); 151 | 152 | } else { 153 | reject({ 154 | status: 'error_not_connected', 155 | error: 'Console is not connected' 156 | }) 157 | } 158 | }.bind(this)) 159 | }, 160 | 161 | connect: function(ip, userhash, xsts_token) 162 | { 163 | this._ip = ip 164 | 165 | return new Promise(function(resolve, reject) { 166 | this.discovery(this._ip).then(function(consoles){ 167 | if(consoles.length > 0){ 168 | Debug('['+this._client_id+'] Console is online. Lets connect...') 169 | // clearTimeout(this._interval_timeout) 170 | 171 | this._getSocket(); 172 | 173 | var xbox = Xbox(consoles[0].remote.address, consoles[0].message.certificate); 174 | var message = xbox.connect(userhash, xsts_token); 175 | 176 | this._send(message); 177 | 178 | this._console = xbox 179 | 180 | this._events.on('_on_connect_response', function(message, xbox, remote, smartglass){ 181 | if(message.packet_decoded.protected_payload.connect_result == '0'){ 182 | Debug('['+this._client_id+'] Console is connected') 183 | this._connection_status = true 184 | resolve() 185 | } else { 186 | this._connection_status = false 187 | 188 | var errorTable = { 189 | 0: 'Success', 190 | 1: 'Pending login. Reconnect to complete', 191 | 2: 'Unknown error', 192 | 3: 'No anonymous connections', 193 | 4: 'Device limit exceeded', 194 | 5: 'Smartglass is disabled on the Xbox console', 195 | 6: 'User authentication failed', 196 | 7: 'Sign-in failed', 197 | 8: 'Sign-in timeout', 198 | 9: 'Sign-in required' 199 | } 200 | 201 | Debug('['+this._client_id+'] Error during connect, xbox returned result:', errorTable[message.packet_decoded.protected_payload.connect_result]) 202 | this._closeClient() 203 | 204 | reject({ 205 | 'error': 'connection_rejected', 206 | 'message': errorTable[message.packet_decoded.protected_payload.connect_result], 207 | 'details': message.packet_decoded.protected_payload 208 | }) 209 | } 210 | }.bind(this)) 211 | 212 | this._events.on('_on_timeout', function(message, xbox, remote, smartglass){ 213 | Debug('['+this._client_id+'] Client timeout...') 214 | this._connection_status = false 215 | 216 | reject({ 217 | 'error': 'connection_timeout', 218 | 'message': 'No response from the xbox' 219 | }) 220 | }.bind(this)) 221 | } else { 222 | Debug('['+this._client_id+'] Device is unavailable...') 223 | this._connection_status = false 224 | 225 | reject({ 226 | 'error': 'device_unavailable', 227 | 'message': 'Xbox is unavailable on '+ip 228 | }) 229 | } 230 | }.bind(this), function(error){ 231 | reject(error) 232 | }) 233 | }.bind(this)) 234 | }, 235 | 236 | on: function(name, callback) 237 | { 238 | this._events.on(name, callback) 239 | }, 240 | 241 | disconnect: function() 242 | { 243 | var xbox = this._console; 244 | 245 | xbox.get_requestnum() 246 | 247 | var disconnect = Packer('message.disconnect') 248 | disconnect.set('reason', 4) 249 | disconnect.set('error_code', 0) 250 | var disconnect_message = disconnect.pack(xbox) 251 | 252 | this._send(disconnect_message); 253 | 254 | this._closeClient() 255 | }, 256 | 257 | recordGameDvr: function() 258 | { 259 | return new Promise(function(resolve, reject) { 260 | if(this.isConnected() == true){ 261 | if(this._console._is_authenticated == true){ 262 | Debug('['+this._client_id+'] Sending record game dvr command') 263 | 264 | this._console.get_requestnum() 265 | var game_dvr_record = Packer('message.game_dvr_record') 266 | game_dvr_record.set('start_time_delta', -60) // Needs to be signed int 267 | game_dvr_record.set('end_time_delta', 0) 268 | var message = game_dvr_record.pack(this._console) 269 | 270 | this._send(message); 271 | 272 | resolve(true) 273 | } else { 274 | reject({ 275 | status: 'error_not_authenticated', 276 | error: 'Game DVR record function requires an authenticated user' 277 | }) 278 | } 279 | } else { 280 | reject({ 281 | status: 'error_not_connected', 282 | error: 'Console is not connected' 283 | }) 284 | } 285 | }.bind(this)) 286 | }, 287 | 288 | addManager: function(name, manager) 289 | { 290 | Debug('Loaded manager: '+name + '('+this._managers_num+')') 291 | this._managers[name] = manager 292 | this._managers[name].load(this, this._managers_num) 293 | this._managers_num++ 294 | }, 295 | 296 | getManager: function(name) 297 | { 298 | if(this._managers[name] != undefined) 299 | return this._managers[name] 300 | else 301 | return false 302 | }, 303 | 304 | _getSocket: function() 305 | { 306 | Debug('['+this._client_id+'] Get active socket'); 307 | 308 | this._socket = dgram.createSocket('udp4'); 309 | this._socket.bind(); 310 | 311 | this._socket.on('listening', function(message, remote){ 312 | if(this._is_broadcast == true) 313 | this._socket.setBroadcast(true); 314 | }.bind(this)) 315 | 316 | this._socket.on('error', function(error){ 317 | Debug('Socket Error:') 318 | Debug(error) 319 | }.bind(this)) 320 | 321 | this._socket.on('message', function(message, remote){ 322 | this._last_received_time = Math.floor(Date.now() / 1000) 323 | var xbox = this._console 324 | this._events.emit('receive', message, xbox, remote, this); 325 | }.bind(this)); 326 | 327 | this._socket.on('close', function() { 328 | Debug('['+this._client_id+'] UDP socket closed.'); 329 | }.bind(this)); 330 | 331 | return this._socket; 332 | }, 333 | 334 | _closeClient: function() 335 | { 336 | Debug('['+this._client_id+'] Client closed'); 337 | this._connection_status = false 338 | 339 | clearInterval(this._interval_timeout) 340 | if(this._socket != false){ 341 | this._socket.close(); 342 | this._socket = false 343 | } 344 | 345 | }, 346 | 347 | _send: function(message, ip) 348 | { 349 | if(ip == undefined){ 350 | ip = this._ip 351 | } 352 | 353 | if(this._socket != false) 354 | this._socket.send(message, 0, message.length, 5050, ip, function(err, bytes) { 355 | Debug('['+this._client_id+'] Sending packet to client: '+this._ip+':'+5050); 356 | Debug(message.toString('hex')) 357 | }.bind(this)); 358 | }, 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /tests/packet_packer_message.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var fs = require('fs'); 3 | const Packer = require('../src/packet/packer'); 4 | var Xbox = require('../src/xbox'); 5 | 6 | var secret = Buffer.from('82bba514e6d19521114940bd65121af2'+'34c53654a8e67add7710b3725db44f77'+'30ed8e3da7015a09fe0f08e9bef3853c0506327eb77c9951769d923d863a2f5e', 'hex'); 7 | var certificate = Buffer.from('041db1e7943878b28c773228ebdcfb05b985be4a386a55f50066231360785f61b60038caf182d712d86c8a28a0e7e2733a0391b1169ef2905e4e21555b432b262d', 'hex'); 8 | 9 | var packets = [ 10 | {'message.console_status': 'tests/data/packets/console_status'}, 11 | {'message.power_off': 'tests/data/packets/poweroff'}, 12 | {'message.acknowledgement': 'tests/data/packets/acknowledge'}, 13 | {'message.local_join': 'tests/data/packets/local_join'}, 14 | {'message.disconnect': 'tests/data/packets/disconnect'}, 15 | {'message.start_channel_request': 'tests/data/packets/start_channel_request'}, 16 | {'message.start_channel_response': 'tests/data/packets/disconnect'}, 17 | {'message.gamepad': 'tests/data/packets/gamepad'}, 18 | {'message.media_state': 'tests/data/packets/media_state'}, 19 | {'message.media_command': 'tests/data/packets/media_command'}, 20 | {'message.json': 'tests/data/packets/json'}, 21 | {'message.game_dvr_record': 'tests/data/packets/gamedvr_record'} 22 | ] 23 | 24 | var device = Xbox('127.0.0.1', certificate); 25 | device.loadCrypto(certificate.toString('hex'), secret.toString('hex')); 26 | 27 | describe('packet/packer/message', function(){ 28 | 29 | it('should unpack a console_status packet', function(){ 30 | var data_packet = fs.readFileSync('tests/data/packets/console_status') 31 | 32 | var poweron_request = Packer(data_packet) 33 | var message = poweron_request.unpack(device) 34 | 35 | assert.deepStrictEqual(message.type, 'message') 36 | assert.deepStrictEqual(message.packet_decoded.sequence_number, 5) 37 | assert.deepStrictEqual(message.packet_decoded.source_participant_id, 0) 38 | assert.deepStrictEqual(message.packet_decoded.target_participant_id, 31) 39 | 40 | assert.deepStrictEqual(message.packet_decoded.flags.version, '2') 41 | assert.deepStrictEqual(message.packet_decoded.flags.need_ack, true) 42 | assert.deepStrictEqual(message.packet_decoded.flags.is_fragment, false) 43 | assert.deepStrictEqual(message.packet_decoded.flags.type, 'console_status') 44 | assert.deepStrictEqual(message.packet_decoded.channel_id, Buffer.from('\x00\x00\x00\x00\x00\x00\x00\x00')) 45 | assert.deepStrictEqual(message.packet_decoded.protected_payload.live_tv_provider, 0) 46 | assert.deepStrictEqual(message.packet_decoded.protected_payload.major_version, 10) 47 | assert.deepStrictEqual(message.packet_decoded.protected_payload.minor_version, 0) 48 | assert.deepStrictEqual(message.packet_decoded.protected_payload.build_number, 14393) 49 | assert.deepStrictEqual(message.packet_decoded.protected_payload.locale, 'en-US') 50 | 51 | assert.deepStrictEqual(message.packet_decoded.protected_payload.apps[0].title_id, 714681658) 52 | assert.deepStrictEqual(message.packet_decoded.protected_payload.apps[0].flags, Buffer.from('8003', 'hex')) 53 | assert.deepStrictEqual(message.packet_decoded.protected_payload.apps[0].product_id, Buffer.from('00000000000000000000000000000000', 'hex')) 54 | assert.deepStrictEqual(message.packet_decoded.protected_payload.apps[0].sandbox_id, Buffer.from('00000000000000000000000000000000', 'hex')) 55 | assert.deepStrictEqual(message.packet_decoded.protected_payload.apps[0].aum_id, 'Xbox.Home_8wekyb3d8bbwe!Xbox.Home.Application') 56 | }); 57 | 58 | it('should unpack a poweroff packet', function(){ 59 | var data_packet = fs.readFileSync('tests/data/packets/poweroff') 60 | 61 | var poweroff_request = Packer(data_packet) 62 | var message = poweroff_request.unpack(device) 63 | 64 | assert.deepStrictEqual(message.type, 'message') 65 | assert.deepStrictEqual(message.packet_decoded.sequence_number, 1882) 66 | assert.deepStrictEqual(message.packet_decoded.source_participant_id, 2) 67 | assert.deepStrictEqual(message.packet_decoded.target_participant_id, 0) 68 | 69 | assert.deepStrictEqual(message.packet_decoded.flags.version, '2') 70 | assert.deepStrictEqual(message.packet_decoded.flags.need_ack, true) 71 | assert.deepStrictEqual(message.packet_decoded.flags.is_fragment, false) 72 | assert.deepStrictEqual(message.packet_decoded.flags.type, 'power_off') 73 | assert.deepStrictEqual(message.packet_decoded.channel_id, Buffer.from('\x00\x00\x00\x00\x00\x00\x00\x00')) 74 | 75 | assert.deepStrictEqual(message.packet_decoded.protected_payload.liveid, 'FD00112233FFEE66') 76 | }); 77 | 78 | it('should unpack an acknowledge packet', function(){ 79 | var data_packet = fs.readFileSync('tests/data/packets/acknowledge') 80 | 81 | var acknowledge = Packer(data_packet) 82 | var message = acknowledge.unpack(device) 83 | 84 | assert.deepStrictEqual(message.type, 'message') 85 | assert.deepStrictEqual(message.packet_decoded.sequence_number, 1) 86 | assert.deepStrictEqual(message.packet_decoded.source_participant_id, 0) 87 | assert.deepStrictEqual(message.packet_decoded.target_participant_id, 31) 88 | 89 | assert.deepStrictEqual(message.packet_decoded.flags.version, '2') 90 | assert.deepStrictEqual(message.packet_decoded.flags.need_ack, false) 91 | assert.deepStrictEqual(message.packet_decoded.flags.is_fragment, false) 92 | assert.deepStrictEqual(message.packet_decoded.flags.type, 'acknowledge') 93 | assert.deepStrictEqual(message.packet_decoded.channel_id, Buffer.from('\x10\x00\x00\x00\x00\x00\x00\x00')) 94 | 95 | assert.deepStrictEqual(message.packet_decoded.protected_payload.low_watermark, 0) 96 | assert.deepStrictEqual(message.packet_decoded.protected_payload.processed_list.length, 1) 97 | assert.deepStrictEqual(message.packet_decoded.protected_payload.rejected_list.length, 0) 98 | assert.deepStrictEqual(message.packet_decoded.protected_payload.processed_list[0].id, 1) 99 | }); 100 | 101 | it('should unpack a local_join packet', function(){ 102 | var data_packet = fs.readFileSync('tests/data/packets/local_join') 103 | 104 | var local_join = Packer(data_packet) 105 | var message = local_join.unpack(device) 106 | 107 | assert.deepStrictEqual(message.type, 'message') 108 | assert.deepStrictEqual(message.packet_decoded.sequence_number, 1) 109 | assert.deepStrictEqual(message.packet_decoded.source_participant_id, 31) 110 | assert.deepStrictEqual(message.packet_decoded.target_participant_id, 0) 111 | 112 | assert.deepStrictEqual(message.packet_decoded.flags.version, '0') 113 | assert.deepStrictEqual(message.packet_decoded.flags.need_ack, true) 114 | assert.deepStrictEqual(message.packet_decoded.flags.is_fragment, false) 115 | assert.deepStrictEqual(message.packet_decoded.flags.type, 'local_join') 116 | assert.deepStrictEqual(message.packet_decoded.channel_id, Buffer.from('\x00\x00\x00\x00\x00\x00\x00\x00')) 117 | 118 | assert.deepStrictEqual(message.packet_decoded.protected_payload.client_type, 8) 119 | assert.deepStrictEqual(message.packet_decoded.protected_payload.native_width, 600) 120 | assert.deepStrictEqual(message.packet_decoded.protected_payload.native_height, 1024) 121 | assert.deepStrictEqual(message.packet_decoded.protected_payload.dpi_x, 160) 122 | assert.deepStrictEqual(message.packet_decoded.protected_payload.dpi_y, 160) 123 | assert.deepStrictEqual(message.packet_decoded.protected_payload.device_capabilities, Buffer.from('ffffffffffffffff', 'hex')) 124 | assert.deepStrictEqual(message.packet_decoded.protected_payload.client_version, 133713371) 125 | assert.deepStrictEqual(message.packet_decoded.protected_payload.os_major_version, 42) 126 | assert.deepStrictEqual(message.packet_decoded.protected_payload.os_minor_version, 0) 127 | assert.deepStrictEqual(message.packet_decoded.protected_payload.display_name, 'package.name.here') 128 | }); 129 | 130 | it('should unpack a disconnect packet', function(){ 131 | var data_packet = fs.readFileSync('tests/data/packets/disconnect') 132 | 133 | var poweroff_request = Packer(data_packet) 134 | var message = poweroff_request.unpack(device) 135 | 136 | assert.deepStrictEqual(message.type, 'message') 137 | assert.deepStrictEqual(message.packet_decoded.sequence_number, 57) 138 | assert.deepStrictEqual(message.packet_decoded.source_participant_id, 31) 139 | assert.deepStrictEqual(message.packet_decoded.target_participant_id, 0) 140 | 141 | assert.deepStrictEqual(message.packet_decoded.flags.version, '2') 142 | assert.deepStrictEqual(message.packet_decoded.flags.need_ack, false) 143 | assert.deepStrictEqual(message.packet_decoded.flags.is_fragment, false) 144 | assert.deepStrictEqual(message.packet_decoded.flags.type, 'disconnect') 145 | assert.deepStrictEqual(message.packet_decoded.channel_id, Buffer.from('\x00\x00\x00\x00\x00\x00\x00\x00')) 146 | 147 | assert.deepStrictEqual(message.packet_decoded.protected_payload.reason, 0) 148 | assert.deepStrictEqual(message.packet_decoded.protected_payload.error_code, 0) 149 | }); 150 | 151 | it('should unpack a start_channel_request packet', function(){ 152 | var data_packet = fs.readFileSync('tests/data/packets/start_channel_request') 153 | 154 | var poweroff_request = Packer(data_packet) 155 | var message = poweroff_request.unpack(device) 156 | 157 | assert.deepStrictEqual(message.type, 'message') 158 | assert.deepStrictEqual(message.packet_decoded.sequence_number, 2) 159 | assert.deepStrictEqual(message.packet_decoded.source_participant_id, 31) 160 | assert.deepStrictEqual(message.packet_decoded.target_participant_id, 0) 161 | 162 | assert.deepStrictEqual(message.packet_decoded.flags.version, '2') 163 | assert.deepStrictEqual(message.packet_decoded.flags.need_ack, true) 164 | assert.deepStrictEqual(message.packet_decoded.flags.is_fragment, false) 165 | assert.deepStrictEqual(message.packet_decoded.flags.type, 'start_channel_request') 166 | assert.deepStrictEqual(message.packet_decoded.channel_id, Buffer.from('\x00\x00\x00\x00\x00\x00\x00\x00')) 167 | 168 | assert.deepStrictEqual(message.packet_decoded.protected_payload.channel_request_id, 1) 169 | assert.deepStrictEqual(message.packet_decoded.protected_payload.title_id, 0) 170 | assert.deepStrictEqual(message.packet_decoded.protected_payload.service, Buffer.from('fa20b8ca66fb46e0adb60b978a59d35f', 'hex')) // SystemInput 171 | assert.deepStrictEqual(message.packet_decoded.protected_payload.activity_id, 0) 172 | }); 173 | 174 | it('should unpack a start_channel_response packet', function(){ 175 | var data_packet = fs.readFileSync('tests/data/packets/start_channel_response') 176 | 177 | var poweroff_request = Packer(data_packet) 178 | var message = poweroff_request.unpack(device) 179 | 180 | assert.deepStrictEqual(message.type, 'message') 181 | assert.deepStrictEqual(message.packet_decoded.sequence_number, 6) 182 | assert.deepStrictEqual(message.packet_decoded.source_participant_id, 0) 183 | assert.deepStrictEqual(message.packet_decoded.target_participant_id, 31) 184 | 185 | assert.deepStrictEqual(message.packet_decoded.flags.version, '2') 186 | assert.deepStrictEqual(message.packet_decoded.flags.need_ack, true) 187 | assert.deepStrictEqual(message.packet_decoded.flags.is_fragment, false) 188 | assert.deepStrictEqual(message.packet_decoded.flags.type, 'start_channel_response') 189 | assert.deepStrictEqual(message.packet_decoded.channel_id, Buffer.from('\x00\x00\x00\x00\x00\x00\x00\x00')) 190 | 191 | assert.deepStrictEqual(message.packet_decoded.protected_payload.channel_request_id, 1) 192 | assert.deepStrictEqual(message.packet_decoded.protected_payload.target_channel_id, Buffer.from('0000000000000094', 'hex')) 193 | assert.deepStrictEqual(message.packet_decoded.protected_payload.result, 0) 194 | }); 195 | 196 | it('should unpack a gamepad packet', function(){ 197 | var data_packet = fs.readFileSync('tests/data/packets/gamepad') 198 | 199 | var poweroff_request = Packer(data_packet) 200 | var message = poweroff_request.unpack(device) 201 | 202 | assert.deepStrictEqual(message.type, 'message') 203 | assert.deepStrictEqual(message.packet_decoded.sequence_number, 79) 204 | assert.deepStrictEqual(message.packet_decoded.source_participant_id, 41) 205 | assert.deepStrictEqual(message.packet_decoded.target_participant_id, 0) 206 | 207 | assert.deepStrictEqual(message.packet_decoded.flags.version, '2') 208 | assert.deepStrictEqual(message.packet_decoded.flags.need_ack, false) 209 | assert.deepStrictEqual(message.packet_decoded.flags.is_fragment, false) 210 | assert.deepStrictEqual(message.packet_decoded.flags.type, 'gamepad') 211 | assert.deepStrictEqual(message.packet_decoded.channel_id, Buffer.from('00000000000000b4', 'hex')) 212 | 213 | assert.deepStrictEqual(message.packet_decoded.protected_payload.timestamp, Buffer.from('0000000000000000', 'hex')) 214 | assert.deepStrictEqual(message.packet_decoded.protected_payload.buttons, 32) 215 | assert.deepStrictEqual(message.packet_decoded.protected_payload.left_trigger, 0) 216 | assert.deepStrictEqual(message.packet_decoded.protected_payload.right_trigger, 0) 217 | assert.deepStrictEqual(message.packet_decoded.protected_payload.left_thumbstick_x, 0) 218 | assert.deepStrictEqual(message.packet_decoded.protected_payload.left_thumbstick_y, 0) 219 | assert.deepStrictEqual(message.packet_decoded.protected_payload.right_thumbstick_x, 0) 220 | assert.deepStrictEqual(message.packet_decoded.protected_payload.right_thumbstick_y, 0) 221 | }); 222 | 223 | it('should unpack a media_state packet', function(){ 224 | var data_packet = fs.readFileSync('tests/data/packets/media_state') 225 | 226 | var poweroff_request = Packer(data_packet) 227 | var message = poweroff_request.unpack(device) 228 | 229 | assert.deepStrictEqual(message.type, 'message') 230 | assert.deepStrictEqual(message.packet_decoded.sequence_number, 158) 231 | assert.deepStrictEqual(message.packet_decoded.source_participant_id, 0) 232 | assert.deepStrictEqual(message.packet_decoded.target_participant_id, 32) 233 | 234 | assert.deepStrictEqual(message.packet_decoded.flags.version, '2') 235 | assert.deepStrictEqual(message.packet_decoded.flags.need_ack, true) 236 | assert.deepStrictEqual(message.packet_decoded.flags.is_fragment, false) 237 | assert.deepStrictEqual(message.packet_decoded.flags.type, 'media_state') 238 | assert.deepStrictEqual(message.packet_decoded.channel_id, Buffer.from('0000000000000099', 'hex')) 239 | 240 | assert.deepStrictEqual(message.packet_decoded.protected_payload.title_id, 274278798) 241 | assert.deepStrictEqual(message.packet_decoded.protected_payload.aum_id, 'AIVDE_s9eep9cpjhg6g!App') 242 | assert.deepStrictEqual(message.packet_decoded.protected_payload.asset_id, '') 243 | assert.deepStrictEqual(message.packet_decoded.protected_payload.media_type, 'No Media') 244 | assert.deepStrictEqual(message.packet_decoded.protected_payload.sound_level, 'Full') 245 | assert.deepStrictEqual(message.packet_decoded.protected_payload.enabled_commands, 33758) 246 | assert.deepStrictEqual(message.packet_decoded.protected_payload.playback_status, 'Stopped') 247 | assert.deepStrictEqual(message.packet_decoded.protected_payload.rate, 0) 248 | }); 249 | 250 | it('should unpack a media_command packet', function(){ 251 | var data_packet = fs.readFileSync('tests/data/packets/media_command') 252 | 253 | var poweroff_request = Packer(data_packet) 254 | var message = poweroff_request.unpack(device) 255 | 256 | assert.deepStrictEqual(message.type, 'message') 257 | assert.deepStrictEqual(message.packet_decoded.sequence_number, 597) 258 | assert.deepStrictEqual(message.packet_decoded.source_participant_id, 32) 259 | assert.deepStrictEqual(message.packet_decoded.target_participant_id, 0) 260 | 261 | assert.deepStrictEqual(message.packet_decoded.flags.version, '2') 262 | assert.deepStrictEqual(message.packet_decoded.flags.need_ack, true) 263 | assert.deepStrictEqual(message.packet_decoded.flags.is_fragment, false) 264 | assert.deepStrictEqual(message.packet_decoded.flags.type, 'media_command') 265 | assert.deepStrictEqual(message.packet_decoded.channel_id, Buffer.from('0000000000000099', 'hex')) 266 | 267 | assert.deepStrictEqual(message.packet_decoded.protected_payload.request_id, Buffer.from('0000000000000000', 'hex')) 268 | assert.deepStrictEqual(message.packet_decoded.protected_payload.title_id, 274278798) 269 | assert.deepStrictEqual(message.packet_decoded.protected_payload.command, 256) 270 | assert.deepStrictEqual(message.packet_decoded.protected_payload.seek_position, undefined) // Should be tested when implemented 271 | }); 272 | 273 | it('should unpack a json packet', function(){ 274 | var data_packet = fs.readFileSync('tests/data/packets/json') 275 | 276 | var poweroff_request = Packer(data_packet) 277 | var message = poweroff_request.unpack(device) 278 | 279 | assert.deepStrictEqual(message.type, 'message') 280 | assert.deepStrictEqual(message.packet_decoded.sequence_number, 11) 281 | assert.deepStrictEqual(message.packet_decoded.source_participant_id, 31) 282 | assert.deepStrictEqual(message.packet_decoded.target_participant_id, 0) 283 | 284 | assert.deepStrictEqual(message.packet_decoded.flags.version, '2') 285 | assert.deepStrictEqual(message.packet_decoded.flags.need_ack, true) 286 | assert.deepStrictEqual(message.packet_decoded.flags.is_fragment, false) 287 | assert.deepStrictEqual(message.packet_decoded.flags.type, 'json') 288 | assert.deepStrictEqual(message.packet_decoded.channel_id, Buffer.from('0000000000000097', 'hex')) 289 | 290 | assert.deepStrictEqual(message.packet_decoded.protected_payload.json, '{"msgid":"2ed6c0fd.2","request":"GetConfiguration"}') 291 | }); 292 | 293 | it('should unpack a game_dvr_record packet', function(){ 294 | var data_packet = fs.readFileSync('tests/data/packets/gamedvr_record') 295 | 296 | var game_dvr = Packer(data_packet) 297 | var message = game_dvr.unpack(device) 298 | 299 | assert.deepStrictEqual(message.type, 'message') 300 | assert.deepStrictEqual(message.packet_decoded.sequence_number, 70) 301 | assert.deepStrictEqual(message.packet_decoded.source_participant_id, 1) 302 | assert.deepStrictEqual(message.packet_decoded.target_participant_id, 0) 303 | 304 | assert.deepStrictEqual(message.packet_decoded.protected_payload.start_time_delta, -60) 305 | assert.deepStrictEqual(message.packet_decoded.protected_payload.end_time_delta, 0) 306 | }); 307 | 308 | describe('should repack messages correctly', function(){ 309 | packets.forEach(function(element, packetType){ 310 | for (var name in element) break; 311 | 312 | it('should repack a valid '+name+' packet', function(){ 313 | var data_packet = fs.readFileSync(element[name]) 314 | // console.log('d_packet', data_packet.toString('hex')); 315 | 316 | var response = Packer(data_packet) 317 | var message = response.unpack(device) 318 | // console.log('d_packet message:', message.packet_decoded.decrypted_payload.toString('hex')); 319 | // console.log(message); 320 | 321 | device._request_num = message.packet_decoded.sequence_number 322 | device._target_participant_id = message.packet_decoded.target_participant_id 323 | device._source_participant_id = message.packet_decoded.source_participant_id 324 | 325 | var repacked = message.pack(device) 326 | // console.log('repacked', repacked.toString('hex')); 327 | 328 | assert.deepStrictEqual(data_packet, Buffer.from(repacked)) 329 | }); 330 | }) 331 | }); 332 | }) 333 | -------------------------------------------------------------------------------- /src/packet/message.js: -------------------------------------------------------------------------------- 1 | var PacketStructure = require('./structure'); 2 | var Packer = require('./packer'); 3 | var hexToBin = require('hex-to-binary'); 4 | var Debug = require('debug')('smartglass:packet_message') 5 | 6 | module.exports = function(type, packet_data = false){ 7 | var Playback_Status = { 8 | 0: 'Closed', 9 | 1: 'Changing', 10 | 2: 'Stopped', 11 | 3: 'Playing', 12 | 4: 'Paused' 13 | } 14 | 15 | var Media_Types = { 16 | 0: 'No Media', 17 | 1: 'Music', 18 | 2: 'Video', 19 | 3: 'Image', 20 | 4: 'Conversation', 21 | 5: 'Game' 22 | } 23 | 24 | var Sound_Status = { 25 | 0: 'Muted', 26 | 1: 'Low', 27 | 2: 'Full' 28 | } 29 | 30 | var Type = { 31 | uInt32: function(value){ 32 | return { 33 | value: value, 34 | pack: function(packet_structure){ 35 | return packet_structure.writeUInt32(this.value); 36 | }, 37 | unpack: function(packet_structure){ 38 | this.value = packet_structure.readUInt32(); 39 | return this.value 40 | } 41 | } 42 | }, 43 | sInt32: function(value){ 44 | return { 45 | value: value, 46 | pack: function(packet_structure){ 47 | return packet_structure.writeInt32(this.value); 48 | }, 49 | unpack: function(packet_structure){ 50 | this.value = packet_structure.readInt32(); 51 | return this.value 52 | } 53 | } 54 | }, 55 | uInt16: function(value){ 56 | return { 57 | value: value, 58 | pack: function(packet_structure){ 59 | return packet_structure.writeUInt16(this.value); 60 | }, 61 | unpack: function(packet_structure){ 62 | this.value = packet_structure.readUInt16(); 63 | return this.value 64 | } 65 | } 66 | }, 67 | bytes: function(length, value){ 68 | return { 69 | value: value, 70 | length: length, 71 | pack: function(packet_structure){ 72 | return packet_structure.writeBytes(this.value); 73 | }, 74 | unpack: function(packet_structure){ 75 | this.value = packet_structure.readBytes(length); 76 | return this.value 77 | } 78 | } 79 | }, 80 | sgString: function(value){ 81 | return { 82 | value: value, 83 | pack: function(packet_structure){ 84 | return packet_structure.writeSGString(this.value); 85 | }, 86 | unpack: function(packet_structure){ 87 | this.value = packet_structure.readSGString().toString(); 88 | return this.value 89 | } 90 | } 91 | }, 92 | flags: function(length, value){ 93 | 94 | return { 95 | value: value, 96 | length: length, 97 | pack: function(packet_structure){ 98 | return packet_structure.writeBytes(setFlags(this.value)); 99 | }, 100 | unpack: function(packet_structure){ 101 | this.value = readFlags(packet_structure.readBytes(this.length)); 102 | return this.value 103 | } 104 | } 105 | }, 106 | sgArray: function(structure, value){ 107 | return { 108 | value: value, 109 | structure: structure, 110 | pack: function(packet_structure){ 111 | // @Todo 112 | 113 | packet_structure.writeUInt16(this.value.length); 114 | 115 | var array_structure = Packet[this.structure]; 116 | for(var index in this.value) 117 | { 118 | for(var name in array_structure){ 119 | array_structure[name].value = this.value[index][name] 120 | packet_structure = array_structure[name].pack(packet_structure) 121 | } 122 | } 123 | 124 | return packet_structure; 125 | }, 126 | unpack: function(packet_structure){ 127 | var array_count = packet_structure.readUInt16(); 128 | var array = [] 129 | 130 | for(var i = 0; i < array_count; i++) { 131 | var array_structure = Packet[this.structure]; 132 | var item = {} 133 | 134 | for(var name in array_structure){ 135 | item[name] = array_structure[name].unpack(packet_structure) 136 | } 137 | 138 | array.push(item) 139 | } 140 | 141 | this.value = array 142 | return this.value; 143 | } 144 | } 145 | }, 146 | sgList: function(structure, value){ 147 | return { 148 | value: value, 149 | structure: structure, 150 | pack: function(packet_structure){ 151 | 152 | packet_structure.writeUInt32(this.value.length); 153 | 154 | var array_structure = Packet[this.structure]; 155 | for(var index in this.value) 156 | { 157 | for(name in array_structure){ 158 | array_structure[name].value = this.value[index][name] 159 | packet_structure = array_structure[name].pack(packet_structure) 160 | } 161 | } 162 | 163 | return packet_structure; 164 | }, 165 | unpack: function(packet_structure){ 166 | var array_count = packet_structure.readUInt32(); 167 | var array = [] 168 | 169 | for(var i = 0; i < array_count; i++) { 170 | var array_structure = Packet[this.structure]; 171 | var item = {} 172 | 173 | for(name in array_structure){ 174 | item[name] = array_structure[name].unpack(packet_structure) 175 | } 176 | 177 | array.push(item) 178 | } 179 | 180 | this.value = array 181 | return this.value; 182 | } 183 | } 184 | }, 185 | mapper: function(map, item){ 186 | return { 187 | item: item, 188 | value: false, 189 | pack: function(packet_structure){ 190 | return item.pack(packet_structure); 191 | }, 192 | unpack: function(packet_structure){ 193 | this.value = item.unpack(packet_structure); 194 | return map[this.value] 195 | } 196 | } 197 | } 198 | } 199 | 200 | var Packet = { 201 | console_status: { 202 | live_tv_provider: Type.uInt32('0'), 203 | major_version: Type.uInt32('0'), 204 | minor_version: Type.uInt32('0'), 205 | build_number: Type.uInt32('0'), 206 | locale: Type.sgString('en-US'), 207 | apps: Type.sgArray('_active_apps') 208 | }, 209 | _active_apps: { 210 | title_id: Type.uInt32('0'), 211 | flags: Type.bytes(2), 212 | product_id: Type.bytes(16, ''), 213 | sandbox_id: Type.bytes(16, ''), 214 | aum_id: Type.sgString('') 215 | }, 216 | power_off: { 217 | liveid: Type.sgString(''), 218 | }, 219 | acknowledge: { 220 | low_watermark: Type.uInt32('0'), 221 | processed_list: Type.sgList('_acknowledge_list', []), 222 | rejected_list: Type.sgList('_acknowledge_list', []), 223 | }, 224 | _acknowledge_list: { 225 | id: Type.uInt32('0'), 226 | }, 227 | game_dvr_record: { 228 | start_time_delta: Type.sInt32('0'), 229 | end_time_delta: Type.sInt32('0'), 230 | }, 231 | start_channel_request: { 232 | channel_request_id: Type.uInt32('0'), 233 | title_id: Type.uInt32('0'), 234 | service: Type.bytes(16, ''), 235 | activity_id: Type.uInt32('0'), 236 | }, 237 | start_channel_response: { 238 | channel_request_id: Type.uInt32('0'), 239 | target_channel_id: Type.bytes(8, ''), 240 | result: Type.uInt32('0'), 241 | }, 242 | gamepad: { 243 | timestamp: Type.bytes(8, ''), 244 | buttons: Type.uInt16('0'), 245 | left_trigger: Type.uInt32('0'), 246 | right_trigger: Type.uInt32('0'), 247 | left_thumbstick_x: Type.uInt32('0'), 248 | left_thumbstick_y: Type.uInt32('0'), 249 | right_thumbstick_x: Type.uInt32('0'), 250 | right_thumbstick_y: Type.uInt32('0'), 251 | }, 252 | media_state: { 253 | title_id: Type.uInt32('0'), 254 | aum_id: Type.sgString(), 255 | asset_id: Type.sgString(), 256 | media_type: Type.mapper(Media_Types, Type.uInt16('0')), 257 | sound_level: Type.mapper(Sound_Status, Type.uInt16('0')), 258 | enabled_commands: Type.uInt32('0'), 259 | playback_status: Type.mapper(Playback_Status, Type.uInt16('0')), 260 | rate: Type.uInt32('0'), 261 | position: Type.bytes(8, ''), 262 | media_start: Type.bytes(8, ''), 263 | media_end: Type.bytes(8, ''), 264 | min_seek: Type.bytes(8, ''), 265 | max_seek: Type.bytes(8, ''), 266 | metadata: Type.sgArray('_media_state_list', []), 267 | }, 268 | _media_state_list: { 269 | name: Type.sgString(), 270 | value: Type.sgString(), 271 | }, 272 | media_command: { 273 | request_id: Type.bytes(8, ''), 274 | title_id: Type.uInt32('0'), 275 | command: Type.uInt32('0'), 276 | }, 277 | local_join: { 278 | client_type: Type.uInt16('3'), 279 | native_width: Type.uInt16('1080'), 280 | native_height: Type.uInt16('1920'), 281 | dpi_x: Type.uInt16('96'), 282 | dpi_y: Type.uInt16('96'), 283 | device_capabilities: Type.bytes(8, Buffer.from('ffffffffffffffff', 'hex')), 284 | client_version: Type.uInt32('15'), 285 | os_major_version: Type.uInt32('6'), 286 | os_minor_version: Type.uInt32('2'), 287 | display_name: Type.sgString('Xbox-Smartglass-Node'), 288 | }, 289 | json: { 290 | json: Type.sgString('{}') 291 | }, 292 | disconnect: { 293 | reason: Type.uInt32('1'), 294 | error_code: Type.uInt32('0') 295 | }, 296 | }; 297 | 298 | function getMsgType(type) 299 | { 300 | var message_types = { 301 | 0x1: "acknowledge", 302 | 0x2: "Group", 303 | 0x3: "local_join", 304 | 0x5: "StopActivity", 305 | 0x19: "AuxilaryStream", 306 | 0x1A: "ActiveSurfaceChange", 307 | 0x1B: "Navigate", 308 | 0x1C: "json", 309 | 0x1D: "Tunnel", 310 | 0x1E: "console_status", 311 | 0x1F: "TitleTextConfiguration", 312 | 0x20: "TitleTextInput", 313 | 0x21: "TitleTextSelection", 314 | 0x22: "MirroringRequest", 315 | 0x23: "TitleLaunch", 316 | 0x26: "start_channel_request", 317 | 0x27: "start_channel_response", 318 | 0x28: "StopChannel", 319 | 0x29: "System", 320 | 0x2A: "disconnect", 321 | 0x2E: "TitleTouch", 322 | 0x2F: "Accelerometer", 323 | 0x30: "Gyrometer", 324 | 0x31: "Inclinometer", 325 | 0x32: "Compass", 326 | 0x33: "Orientation", 327 | 0x36: "PairedIdentityStateChanged", 328 | 0x37: "Unsnap", 329 | 0x38: "game_dvr_record", 330 | 0x39: "power_off", 331 | 0xF00: "MediaControllerRemoved", 332 | 0xF01: "media_command", 333 | 0xF02: "media_command_result", 334 | 0xF03: "media_state", 335 | 0xF0A: "gamepad", 336 | 0xF2B: "SystemTextConfiguration", 337 | 0xF2C: "SystemTextInput", 338 | 0xF2E: "SystemTouch", 339 | 0xF34: "SystemTextAck", 340 | 0xF35: "SystemTextDone" 341 | } 342 | 343 | return message_types[type]; 344 | } 345 | 346 | function readFlags(flags) 347 | { 348 | flags = hexToBin(flags.toString('hex')); 349 | 350 | var need_ack = false 351 | var is_fragment = false; 352 | 353 | if(flags.slice(2, 3) == 1) 354 | need_ack = true; 355 | 356 | if(flags.slice(3, 4) == 1) 357 | is_fragment = true; 358 | 359 | 360 | var type = getMsgType(parseInt(flags.slice(4, 16), 2)) 361 | 362 | return { 363 | 'version': parseInt(flags.slice(0, 2), 2).toString(), 364 | 'need_ack': need_ack, 365 | 'is_fragment': is_fragment, 366 | 'type': type 367 | } 368 | } 369 | 370 | function setFlags(type) 371 | { 372 | var message_flags = { 373 | acknowledge: Buffer.from('8001', 'hex'), 374 | 0x2: "Group", 375 | local_join: Buffer.from('2003', 'hex'), 376 | 0x5: "StopActivity", 377 | 0x19: "AuxilaryStream", 378 | 0x1A: "ActiveSurfaceChange", 379 | 0x1B: "Navigate", 380 | json: Buffer.from('a01c', 'hex'), 381 | 0x1D: "Tunnel", 382 | console_status: Buffer.from('a01e', 'hex'), 383 | 0x1F: "TitleTextConfiguration", 384 | 0x20: "TitleTextInput", 385 | 0x21: "TitleTextSelection", 386 | 0x22: "MirroringRequest", 387 | 0x23: "TitleLaunch", 388 | start_channel_request: Buffer.from('a026', 'hex'), 389 | start_channel_response: Buffer.from('a027', 'hex'), 390 | 0x28: "StopChannel", 391 | 0x29: "System", 392 | disconnect: Buffer.from('802a', 'hex'), 393 | 0x2E: "TitleTouch", 394 | 0x2F: "Accelerometer", 395 | 0x30: "Gyrometer", 396 | 0x31: "Inclinometer", 397 | 0x32: "Compass", 398 | 0x33: "Orientation", 399 | 0x36: "PairedIdentityStateChanged", 400 | 0x37: "Unsnap", 401 | game_dvr_record: Buffer.from('a038', 'hex'), 402 | power_off: Buffer.from('a039', 'hex'), 403 | 0xF00: "MediaControllerRemoved", 404 | media_command: Buffer.from('af01', 'hex'), 405 | media_command_result: Buffer.from('af02', 'hex'), 406 | media_state: Buffer.from('af03', 'hex'), 407 | gamepad: Buffer.from('8f0a', 'hex'), 408 | 0xF2B: "SystemTextConfiguration", 409 | 0xF2C: "SystemTextInput", 410 | 0xF2E: "SystemTouch", 411 | 0xF34: "SystemTextAck", 412 | 0xF35: "SystemTextDone" 413 | } 414 | 415 | return message_flags[type] 416 | } 417 | 418 | var structure = Packet[type]; 419 | 420 | return { 421 | type: 'message', 422 | name: type, 423 | structure: structure, 424 | packet_data: packet_data, 425 | packet_decoded: false, 426 | 427 | channel_id: Buffer.from('\x00\x00\x00\x00\x00\x00\x00\x00'), 428 | 429 | setChannel: function(channel){ 430 | Debug('Set channel to: '+channel.toString('hex')) 431 | this.channel_id = Buffer.from(channel) 432 | }, 433 | 434 | set: function(key, value, subkey = false){ 435 | Debug('['+this.name+']', 'Set:', key, '=', value) 436 | if(subkey == false){ 437 | this.structure[key].value = value 438 | } else { 439 | this.structure[subkey][key].value = value 440 | } 441 | }, 442 | 443 | unpack: function(device = undefined){ 444 | var payload = PacketStructure(this.packet_data) 445 | 446 | var packet = { 447 | type: payload.readBytes(2).toString('hex'), 448 | payload_length: payload.readUInt16(), 449 | sequence_number: payload.readUInt32(), 450 | target_participant_id: payload.readUInt32(), 451 | source_participant_id: payload.readUInt32(), 452 | flags: readFlags(payload.readBytes(2)), 453 | channel_id: payload.readBytes(8), 454 | protected_payload: payload.readBytes() 455 | } 456 | 457 | this.setChannel(packet.channel_id); 458 | 459 | packet.name = packet.flags.type 460 | this.name = packet.flags.type 461 | //console.log('packet type:', packet) 462 | packet.protected_payload = Buffer.from(packet.protected_payload.slice(0, -32)); 463 | packet.signature = packet.protected_payload.slice(-32) 464 | 465 | Debug('Unpacking message:', this.name); 466 | 467 | // Lets decrypt the data when the payload is encrypted 468 | if(packet.protected_payload != undefined){ 469 | var key = device._crypto._encrypt(this.packet_data.slice(0, 16), device._crypto.getIv()); 470 | 471 | var decrypted_payload = device._crypto._decrypt(packet.protected_payload, key); 472 | packet.decrypted_payload = PacketStructure(decrypted_payload).toBuffer() 473 | decrypted_payload = PacketStructure(decrypted_payload) 474 | 475 | this.structure = Packet[packet.name] 476 | var protected_structure = Packet[packet.name] 477 | packet['protected_payload'] = {} 478 | 479 | for(name in protected_structure){ 480 | packet.protected_payload[name] = protected_structure[name].unpack(decrypted_payload) 481 | } 482 | } 483 | 484 | this.packet_decoded = packet; 485 | 486 | return this; 487 | }, 488 | 489 | pack: function(device){ 490 | Debug('Packing message:', this.name); 491 | 492 | var payload = PacketStructure() 493 | 494 | for(name in this.structure){ 495 | this.structure[name].pack(payload) 496 | } 497 | 498 | var header = PacketStructure() 499 | header.writeBytes(Buffer.from('d00d', 'hex')) 500 | header.writeUInt16(payload.toBuffer().length) 501 | header.writeUInt32(device._request_num) 502 | header.writeUInt32(device._target_participant_id) 503 | header.writeUInt32(device._source_participant_id) 504 | header.writeBytes(setFlags(this.name)) 505 | header.writeBytes(this.channel_id) 506 | 507 | if(payload.toBuffer().length % 16 > 0) 508 | { 509 | var padStart = payload.toBuffer().length % 16; 510 | var padTotal = (16-padStart); 511 | for(var paddingnum = (padStart+1); paddingnum <= 16; paddingnum++) 512 | { 513 | payload.writeUInt8(padTotal); 514 | } 515 | } 516 | 517 | var key = device._crypto._encrypt(header.toBuffer().slice(0, 16), device._crypto.getIv()); 518 | var encrypted_payload = device._crypto._encrypt(payload.toBuffer(), device._crypto.getEncryptionKey(), key); 519 | 520 | var packet = Buffer.concat([ 521 | header.toBuffer(), 522 | encrypted_payload 523 | ]); 524 | 525 | var protected_payload_hash = device._crypto._sign(packet); 526 | packet = Buffer.concat([ 527 | packet, 528 | Buffer.from(protected_payload_hash) 529 | ]); 530 | 531 | return packet; 532 | } 533 | } 534 | } 535 | --------------------------------------------------------------------------------