-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
442 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
#!/usr/bin/env node | ||
|
||
import { createSigningCredentials,executeDeviceRekey, signPackage } from '../roku/roku-api'; | ||
import { logSuccess, logInfo, logError } from '../utils/pretty-log'; | ||
|
||
const argv = require('yargs/yargs')(process.argv.slice(2)) | ||
.command( | ||
'sign', | ||
'sign a roku package', | ||
{ | ||
name: { | ||
alias: 'n', | ||
describe: 'application package name (without .pkg)', | ||
default: 'app', | ||
type: 'string' | ||
}, | ||
path: { | ||
alias: 'p', | ||
demandOption: true, | ||
describe: 'path to roku project', | ||
normalize: true, | ||
type: 'string' | ||
}, | ||
signing: { | ||
alias: 's', | ||
demandOption: true, | ||
describe: 'path to signing properties folder', | ||
normalize: true, | ||
type: 'string' | ||
}, | ||
output: { | ||
alias: 'o', | ||
describe: 'path to where the package should be saved', | ||
default: './', | ||
normalize: true, | ||
type: 'string' | ||
} | ||
}, async (argv: { name:string, path:string, signing:string, output:string, device?:string, password?:string, username:string }) => { | ||
try { | ||
const outputPath = await signPackage(argv.path, argv.signing, argv.output, argv.name, argv) | ||
logSuccess(`Package signed and stored at: ${outputPath}`) | ||
} catch (error) { | ||
logError(error.message) | ||
} | ||
}) | ||
.command( | ||
'rekey', | ||
'rekey roku device', | ||
{ | ||
signing: { | ||
alias: 's', | ||
demandOption: true, | ||
describe: 'path to signing properties folder', | ||
normalize: true, | ||
type: 'string' | ||
} | ||
}, async (argv: { signing:string, device?:string, password?:string, username:string }) => { | ||
try { | ||
await executeDeviceRekey(argv.signing, argv) | ||
logSuccess('Device rekey applied.') | ||
} catch(error) { | ||
console.log(error); | ||
logError(error.message) | ||
} | ||
}) | ||
.command( | ||
'create-signing-credentials', | ||
'creates new signing properties for an app (dev_id, password, package)', | ||
{ | ||
name: { | ||
alias: 'n', | ||
describe: 'output package file name', | ||
default: 'app', | ||
type: 'string' | ||
}, | ||
output: { | ||
alias: 'o', | ||
describe: 'path to where signing properties should be saved', | ||
default: './', | ||
normalize: true, | ||
type: 'string' | ||
} | ||
}, async (argv: { name:string, output:string, device?:string, password?:string, username:string }) => { | ||
try { | ||
const outputPath = await createSigningCredentials(argv.name, argv.output, argv) | ||
logSuccess(`Signing credentials generated and stored at: ${outputPath}`) | ||
} catch(error) { | ||
logError(error.message) | ||
} | ||
}) | ||
.options({ | ||
'device': { | ||
alias: 'd', | ||
describe: 'network address of the roku device. also could be set set through the environment variable: ROKU_DEVICE_ADDRESS', | ||
type: 'string' | ||
}, | ||
'password': { | ||
alias: 'w', | ||
describe: 'password of the roku device. also could be set through the environment variable: ROKU_DEVICE_PASSWORD', | ||
type: 'string' | ||
}, | ||
'username': { | ||
alias: 'u', | ||
describe: 'password of the roku device. also could be set through the environment variable: ROKU_DEVICE_PASSWORD', | ||
default: 'rokudev', | ||
type: 'string' | ||
} | ||
}) | ||
.demandCommand(1) | ||
.strict() | ||
.help() | ||
.argv |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
import path from 'path'; | ||
import fs from 'fs'; | ||
import { rekeyDevice, deployAndSignPackage, deleteInstalledChannel } from 'roku-deploy'; | ||
|
||
import { generateKey } from '../roku/roku-genkey'; | ||
|
||
type DeviceProperties = { device?:string, password?:string , username?:string} | ||
|
||
const CREDENTIALS_FILENAME = 'credentials.json' | ||
const PACKAGE_EXTENSION = '.pkg' | ||
|
||
const RESOURCE_FOLDER_PATH = '../resources' | ||
const SIGNING_PROJECT_PATH = 'signing-project' | ||
|
||
const DEFAULT_OUTPUT_DIRECTORY = 'out' | ||
|
||
export async function signPackage(projectPath: string, signingPath: string, outputPath: string, packageName:string, deviceProperties?:DeviceProperties) { | ||
const finalDeviceProperties = getDeviceProperties(deviceProperties?.device, deviceProperties?.password, deviceProperties?.username) | ||
// Clear current installed channel [AR] | ||
deleteInstalledChannel({...finalDeviceProperties}) | ||
|
||
// Rekey device to application signing properties [AR] | ||
const signingProperties = parseSigningProperties(signingPath) | ||
|
||
await rekeyDevice({ | ||
...finalDeviceProperties, | ||
signingPassword: signingProperties.credentials.password, | ||
rekeySignedPackage: path.resolve(signingProperties.packageFilePath), | ||
devId: signingProperties.credentials.dev_id | ||
}) | ||
|
||
// Generate new package [AR] | ||
const generatedPackagePath = await deployAndSignPackage({ | ||
...finalDeviceProperties, | ||
project: `${projectPath}/bsconfig.json`, | ||
rootDir: projectPath, | ||
signingPassword: signingProperties.credentials.password, | ||
devId: signingProperties.credentials.dev_id | ||
}); | ||
|
||
// Create output path directory if it doesn't exist [AR] | ||
if (!fs.existsSync(outputPath)) { | ||
fs.mkdirSync(outputPath, { recursive: true }) | ||
} | ||
// Copy generated package to output path [AR] | ||
const packageOutputPath = path.join(outputPath, `${packageName}${PACKAGE_EXTENSION}`); | ||
fs.copyFileSync(generatedPackagePath, packageOutputPath) | ||
|
||
|
||
// Clear ./out directory | ||
cleanOutDirectory() | ||
|
||
return packageOutputPath | ||
} | ||
|
||
|
||
export async function createSigningCredentials(packageName: string, outputPath: string, deviceProperties?:DeviceProperties) { | ||
|
||
const finalDeviceProperties = getDeviceProperties(deviceProperties?.device, deviceProperties?.password, deviceProperties?.username) | ||
|
||
// Clear current installed channel [AR] | ||
deleteInstalledChannel({...finalDeviceProperties}) | ||
|
||
const signingProperties = await generateKey(finalDeviceProperties.host) | ||
|
||
const signingProjectPath = getResourceAt(SIGNING_PROJECT_PATH) | ||
|
||
const packagePath = await deployAndSignPackage({ | ||
...finalDeviceProperties, | ||
rootDir: signingProjectPath, | ||
signingPassword: signingProperties.password, | ||
devId: signingProperties.dev_id | ||
}); | ||
|
||
const outputSigningPath = path.join(outputPath) | ||
const outputPackagePath = path.join(outputSigningPath, `${packageName}${PACKAGE_EXTENSION}`) | ||
const outputCredentialsPath = path.join(outputSigningPath, CREDENTIALS_FILENAME) | ||
|
||
if (!fs.existsSync(outputSigningPath)) { | ||
fs.mkdirSync(outputSigningPath, { recursive: true }) | ||
} | ||
fs.copyFileSync(packagePath, outputPackagePath) | ||
fs.writeFileSync(outputCredentialsPath, JSON.stringify(signingProperties)) | ||
|
||
return outputPath | ||
} | ||
|
||
export async function executeDeviceRekey(signingPath: string, deviceProperties?: DeviceProperties) { | ||
|
||
const signingProperties = parseSigningProperties(signingPath) | ||
|
||
await rekeyDevice({ | ||
...getDeviceProperties(deviceProperties?.device, deviceProperties?.password, deviceProperties?.username), | ||
signingPassword: signingProperties.credentials.password, | ||
rekeySignedPackage: path.resolve(signingProperties.packageFilePath), | ||
devId: signingProperties.credentials.dev_id | ||
}) | ||
} | ||
|
||
function parseSigningProperties(signingPropertiesPath: string) { | ||
var files = fs.readdirSync(signingPropertiesPath); | ||
const packageFilename = files.find((filePath) => path.extname(filePath) == PACKAGE_EXTENSION); | ||
if (!packageFilename) { | ||
throw Error(`Could not find package at path: ${signingPropertiesPath}`) | ||
} | ||
const packageFilePath = path.join(signingPropertiesPath, packageFilename) | ||
|
||
const credentialsFilePath = path.join(signingPropertiesPath, CREDENTIALS_FILENAME) | ||
if (!path.resolve(credentialsFilePath)) { | ||
throw Error(`Could not find credentials.json at path: ${credentialsFilePath}`) | ||
} | ||
|
||
const credentialsData = fs.readFileSync(credentialsFilePath, 'utf-8') | ||
try { | ||
const credentials = JSON.parse(credentialsData) | ||
if (credentials.dev_id && credentials.password) { | ||
return { credentials, packageFilePath } | ||
} else { | ||
|
||
throw Error(`Missing required keys on credentials.json: dev_id, password`) | ||
} | ||
} catch (error) { | ||
throw Error(`Could not parse credentials file: ${credentialsFilePath}`) | ||
} | ||
} | ||
|
||
function getResourceAt(resourcePath: string) { | ||
const packagePath = '${__dirname}' | ||
const resourcesFolderPath = path.join(packagePath, RESOURCE_FOLDER_PATH) | ||
return path.resolve(path.join(resourcesFolderPath, resourcePath)) | ||
} | ||
|
||
function getDeviceProperties(device: string | undefined = undefined, password: string | undefined = undefined, username: string | undefined = undefined) { | ||
const env = process.env | ||
const finalHost = device ? device : env.ROKU_DEVICE_ADDRESS | ||
const finalUsername = username ? username : env.ROKU_DEVICE_USERNAME | ||
const finalPassword = password ? password : env.ROKU_DEVICE_PASSWORD | ||
|
||
if (finalHost && finalUsername && finalPassword) { | ||
return { host:finalHost, username:finalUsername, password:finalPassword } | ||
} else { | ||
throw Error(`The following device properties should be set: device, password`) | ||
} | ||
} | ||
|
||
function cleanOutDirectory() { | ||
fs.rmSync(path.resolve(DEFAULT_OUTPUT_DIRECTORY), { recursive: true }) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import Telnet from 'telnet-client'; | ||
|
||
|
||
export async function generateKey(deviceAddress: string) { | ||
let connection = new Telnet(); | ||
let params = { | ||
host: deviceAddress, | ||
port: 8080, | ||
shellPrompt: '>', | ||
timeout: 5000, | ||
execTimeout: 5000 | ||
} | ||
|
||
try { | ||
await connection.connect(params); | ||
const response = await connection.exec('genkey'); | ||
await connection.destroy(); | ||
|
||
const password = extractVariable("Password", response); | ||
const dev_id = extractVariable("DevID", response); | ||
|
||
if (password && dev_id) { | ||
return { dev_id, password } | ||
} else { | ||
throw Error(`Could not generate key: Failed to retrieve DevID/password from Roku device[${deviceAddress}].`) | ||
} | ||
|
||
} catch (error) { | ||
throw Error(`Could not generate key: Failed to connect to Roku device[${deviceAddress}].`) | ||
} | ||
} | ||
|
||
function extractVariable(variableName: string, response: string): string | undefined { | ||
const variableRegex = new RegExp(`${variableName}: ([^\r\n]*)`); | ||
const matches = variableRegex.exec(response); | ||
if (matches) { | ||
const value = matches[1]; | ||
return value; | ||
} | ||
|
||
return undefined; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import chalk from 'chalk'; | ||
|
||
const TOOL_PREFIX = '[roku-cli]' | ||
|
||
type LogStyle = { prefix:string, color:chalk.Chalk } | ||
|
||
const WARNING:LogStyle = { prefix: '[WARNING]', color: chalk.yellow }; | ||
const INFO:LogStyle = { prefix: '[INFO]', color: chalk.blue }; | ||
const SUCCESS:LogStyle = { prefix: '[SUCCESS]', color: chalk.green }; | ||
const ERROR:LogStyle = { prefix: '[ERROR]', color: chalk.red }; | ||
|
||
export function logWarning(message:string) { | ||
prettyLog(WARNING, message) | ||
} | ||
|
||
export function logInfo(message:string) { | ||
prettyLog(INFO, message) | ||
} | ||
|
||
export function logSuccess(message?:string) { | ||
prettyLog(SUCCESS, message) | ||
} | ||
|
||
export function logError(message:string) { | ||
prettyLog(ERROR, message) | ||
} | ||
|
||
|
||
function prettyLog(style: LogStyle, message?:string) { | ||
console.log(style.color(`${TOOL_PREFIX}${style.prefix}${ message ? `: ${message}`: ''}`)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import fs from 'fs'; | ||
import path from 'path'; | ||
|
||
import { createSigningCredentials } from '../src/roku/roku-api'; | ||
|
||
const testDeviceProperties = { | ||
device: '192.168.0.6', | ||
username:'rokudev', | ||
password: '4551' | ||
} | ||
|
||
describe('create signing credentials tests', () => { | ||
test('create signing credentials with no device properties should fail', async () => { | ||
try { | ||
await createSigningCredentials('test_app', './out/tests/signing') | ||
fail('create credentials should have failed') | ||
} catch (error) { | ||
expect(error.message).toEqual('The following device properties should be set: device, password') | ||
} | ||
}); | ||
|
||
test('create signing credentials should succeed', async () => { | ||
try { | ||
const appName = 'test_app' | ||
const credentialsPath = await createSigningCredentials(appName, './out/tests/signing', testDeviceProperties) | ||
expect(fs.existsSync(credentialsPath)).toEqual(true) | ||
expect(fs.existsSync(path.join(credentialsPath, `${appName}.pkg`))) .toEqual(true) | ||
expect(fs.existsSync(path.join(credentialsPath, 'credentials.json'))) .toEqual(true) | ||
} catch(error) { | ||
fail(error.message) | ||
} | ||
}, 30000); | ||
}); |
Oops, something went wrong.