Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for cga4233de #2

Merged
merged 10 commits into from
Apr 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
{
"extends": [
"oclif",
"oclif-typescript"
"oclif-typescript",
"plugin:@typescript-eslint/recommended"
],
"rules": {}
"rules": {
"no-useless-constructor": "off",
"indent": [
"warn",
2
],
"lines-between-class-members": "off"
}
}
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
vodafone-station-cli
====================

Access your Arris TG3442DE (aka Vodafone Station) from the comfort of the command line.
Access your Arris TG3442DE or Technicolor CGA4322DE (aka Vodafone Station) from the comfort of the command line.

![ci-status](https://github.com/totev/vodafone-station-cli/actions/workflows/main.yml/badge.svg)
[![oclif](https://img.shields.io/badge/cli-oclif-brightgreen.svg)](https://oclif.io)
Expand All @@ -15,9 +15,10 @@ Access your Arris TG3442DE (aka Vodafone Station) from the comfort of the comman

# Supported hardware

Currently only the following hardware/software is supported:
Currently the following hardware/software is supported:

- Arris TG3442DE running `AR01.02.068.11_092320_711.PC20.10`
- Technicolor CGA4322DE running `1.0.9-IMS-KDG`

# Notes

Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "vodafone-station-cli",
"description": "Access your Arris TG3442DE (aka Vodafone Station) from the comfort of the command line.",
"version": "1.0.0",
"description": "Access your Vodafone Station from the comfort of the command line.",
"version": "1.1.0",
"author": "Dobroslav Totev",
"bin": {
"vodafone-station-cli": "./bin/run"
Expand Down Expand Up @@ -66,4 +66,4 @@
"lint": "eslint . --ext .ts --config .eslintrc"
},
"types": "lib/index.d.ts"
}
}
5 changes: 4 additions & 1 deletion src/base-command.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import Command from '@oclif/command'
import {config} from 'dotenv'
import {Log, OclifLogger} from './logger'
config()

export default abstract class extends Command {

get logger(): Log {
return new OclifLogger(this.log, this.warn, this.debug, this.error)
}
}
28 changes: 28 additions & 0 deletions src/commands/discover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Command from '../base-command'
import {discoverModemIp, ModemDiscovery} from '../discovery'

export default class Discover extends Command {
static description =
'Try to discover a cable modem in the network';

static examples = [
'$ vodafone-station-cli discover',
];

async discoverModem(): Promise<void> {
try {
const modemIp = await discoverModemIp()
this.log(`Possibly found modem under the following IP: ${modemIp}`)
const modem = new ModemDiscovery(modemIp, this.logger)
const discoveredModem = await modem.discover()
this.log(`Discovered modem: ${JSON.stringify(discoveredModem)}`)
} catch (error) {
this.log('Something went wrong.', error)
}
}

async run(): Promise<void> {
await this.discoverModem()
this.exit()
}
}
24 changes: 14 additions & 10 deletions src/commands/docsis.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {flags} from '@oclif/command'
import {promises as fsp} from 'fs'
import Command from '../base-command'
import {CliClient} from '../client'
import {discoverModemIp} from '../discovery'
import {OclifLogger} from '../logger'
import {discoverModemIp, ModemDiscovery} from '../discovery'
import {DocsisStatus} from '../modem'
import {modemFactory} from '../modem/factory'

export default class Docsis extends Command {
static description =
Expand All @@ -26,15 +26,19 @@ JSON data
}),
};

async getDocsisStatus(password: string) {
const cliClient = new CliClient(await discoverModemIp(), new OclifLogger(this.log, this.warn, this.debug, this.error))
async getDocsisStatus(password: string): Promise<DocsisStatus> {
const modemIp = await discoverModemIp()
const discoveredModem = await new ModemDiscovery(modemIp, this.logger).discover()
const modem = modemFactory(discoveredModem)
try {
const csrfNonce = await cliClient.login(password)
return cliClient.fetchDocsisStatus(csrfNonce)
await modem.login(password)
const docsisData = await modem.docsis()
return docsisData
} catch (error) {
this.error('Something went wrong.', error)
throw new Error('Could not fetch docsis status from modem')
} finally {
await cliClient.logout()
await modem.logout()
}
}

Expand All @@ -45,10 +49,10 @@ JSON data
return fsp.writeFile(reportFile, data)
}

async run() {
async run(): Promise<void> {
const {flags} = this.parse(Docsis)

const password = process.env.VODAFONE_ROUTER_PASSWORD ?? flags.password
const password = flags.password ?? process.env.VODAFONE_ROUTER_PASSWORD
if (!password || password === '') {
this.log(
'You must provide a password either using -p or by setting the environment variable VODAFONE_ROUTER_PASSWORD'
Expand Down
24 changes: 13 additions & 11 deletions src/commands/restart.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import {flags} from '@oclif/command'
import Command from '../base-command'
import {CliClient} from '../client'
import {discoverModemIp} from '../discovery'
import {OclifLogger} from '../logger'
import {discoverModemIp, ModemDiscovery} from '../discovery'
import {modemFactory} from '../modem/factory'

export default class Restart extends Command {
static description =
Expand All @@ -19,29 +18,32 @@ export default class Restart extends Command {
}),
};

async restartRouter(password: string) {
const cliClient = new CliClient(await discoverModemIp(), new OclifLogger(this.log, this.warn, this.debug, this.error))
async restartRouter(password: string): Promise<unknown> {
const modemIp = await discoverModemIp()
const discoveredModem = await new ModemDiscovery(modemIp, this.logger).discover()
const modem = modemFactory(discoveredModem)
try {
const csrfNonce = await cliClient.login(password)
await cliClient.restart(csrfNonce)
await modem.login(password)
const restart = await modem.restart()
return restart
} catch (error) {
this.log('Something went wrong.', error)
} finally {
await cliClient.logout()
await modem.logout()
}
}

async run() {
async run(): Promise<void> {
const {flags} = this.parse(Restart)

const password = process.env.VODAFONE_ROUTER_PASSWORD ?? flags.password
const password = flags.password ?? process.env.VODAFONE_ROUTER_PASSWORD
if (!password || password === '') {
this.log(
'You must provide a password either using -p or by setting the environment variable VODAFONE_ROUTER_PASSWORD'
)
this.exit()
}
this.log('Restarting router...')
this.log('Restarting router... this could take some time...')
await this.restartRouter(password)
this.exit()
}
Expand Down
18 changes: 17 additions & 1 deletion src/crypto.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {decrypt, deriveKey, encrypt} from './crypto'
import {decrypt, deriveKey, deriveKeyTechnicolor, encrypt} from './crypto'
import {CryptoVars} from './html-parser'

describe('crypto', () => {
Expand All @@ -13,6 +13,22 @@ describe('crypto', () => {
test('deriveKey', () => {
expect(deriveKey('test', cryptoVars.salt)).toEqual(testPasswordAsKey)
})

test('deriveKey from technicolor', () => {
const password = 'as'
const salt = 'HSts76GJOAB'
const expected = 'bcdf6051836bf84744229389ccc96896'
expect(deriveKeyTechnicolor(password, salt)).toBe(expected)
})

test('deriveKey from technicolor 2times with saltwebui', () => {
const password = 'test'
const salt = 'HSts76GJOAB'
const saltwebui = 'KoD4Sga9fw1K'
const expected = 'd1f11af69dddb4e66ca029ccba4571d4'
expect(deriveKeyTechnicolor(deriveKeyTechnicolor(password, salt), saltwebui)).toBe(expected)
})

test('encrypt', () => {
expect(
encrypt(testPasswordAsKey, 'textToEncrypt', cryptoVars.iv, 'authData')
Expand Down
6 changes: 6 additions & 0 deletions src/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ export function deriveKey(password: string, salt: string) {
return sjcl.codec.hex.fromBits(derivedKeyBits)
}

export function deriveKeyTechnicolor(password: string, salt: string): string {
const derivedKeyBits = sjcl.misc.pbkdf2(password, salt, SJCL_ITERATIONS,
SJCL_KEYSIZEBITS)
return sjcl.codec.hex.fromBits(derivedKeyBits)
}

export function encrypt(
derivedKey: string,
plainText: string,
Expand Down
56 changes: 56 additions & 0 deletions src/discovery.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import axios from 'axios'
import {extractFirmwareVersion} from './html-parser'
import {Log} from './logger'
import {TechnicolorConfiguration} from './modem/technicolor-modem'
const BRIDGED_MODEM_IP = '192.168.100.1'
const ROUTER_IP = '192.168.0.1'

Expand All @@ -13,3 +16,56 @@ export async function discoverModemIp(): Promise<string> {
throw error
}
}

export interface ModemInformation{
deviceType: 'Arris' | 'Technicolor';
firmwareVersion: string;
ipAddress: string;
}

export class ModemDiscovery {
constructor(private readonly modemIp: string, private readonly logger: Log) {}

async tryTechnicolor(): Promise<ModemInformation> {
const {data} = await axios.get<TechnicolorConfiguration>(`http://${this.modemIp}/api/v1/login_conf`)
this.logger.debug(`Technicolor login configuration: ${JSON.stringify(data)}`)
if (data.error === 'ok' && data.data?.firmwareversion) {
return {
deviceType: 'Technicolor',
firmwareVersion: data.data.firmwareversion,
ipAddress: this.modemIp,
}
}
throw new Error('Could not determine modem type')
}

async tryArris(): Promise<ModemInformation> {
const {data} = await axios.get(`http://${this.modemIp}/index.php`, {
headers: {Accept: 'text/html,application/xhtml+xml,application/xml',
},
})
const firmwareVersion = extractFirmwareVersion(data)
if (!firmwareVersion) {
throw new Error('Unable to parse firmware version.')
}
return {
deviceType: 'Arris',
firmwareVersion,
ipAddress: this.modemIp,
}
}

async discover(): Promise<ModemInformation> {
try {
const discovery = await Promise.allSettled([this.tryArris(), this.tryTechnicolor()])
const maybeModem = discovery.find(fam => fam.status === 'fulfilled') as PromiseFulfilledResult<ModemInformation> | undefined
if (!maybeModem) {
throw new Error('Modem discovery was unsuccessful')
}
return maybeModem.value
} catch (error) {
this.logger.warn('Could not find a router/modem under the known addresses')
throw error
}
}
}
5 changes: 5 additions & 0 deletions src/html-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
extractCredentialString,
extractCryptoVars,
extractDocsisStatus,
extractFirmwareVersion,
} from './html-parser'

describe('htmlParser', () => {
Expand Down Expand Up @@ -44,4 +45,8 @@ describe('htmlParser', () => {
'someRandomCatchyHash37f1f79255b66b5c02348e3dc6ff5fcd559654e2'
)
})

test('extractFirmwareVersion', () => {
expect(extractFirmwareVersion(fixtureIndex)).toBe('01.02.068.11.EURO.PC20')
})
})
22 changes: 5 additions & 17 deletions src/html-parser.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import {DocsisChannelStatus, DocsisStatus} from './modem'

const nonceMatcher = /var csp_nonce = "(?<nonce>.*?)";/gm
const ivMatcher = /var myIv = ["|'](?<iv>.*?)["|'];/gm
const saltMatcher = /var mySalt = ["|'](?<salt>.*?)["|'];/gm
const sessionIdMatcher = /var currentSessionId = ["|'](?<sessionId>.*?)["|'];/gm
const swVersionMatcher = /_ga.swVersion = ["|'](?<swVersion>.*?)["|'];/gm

export interface CryptoVars {
nonce: string;
Expand All @@ -18,23 +21,8 @@ export function extractCryptoVars(html: string): CryptoVars {
return {nonce, iv, salt, sessionId} as CryptoVars
}

export interface DocsisStatus {
downstream: DocsisChannelStatus[];
upstream: DocsisChannelStatus[];
downstreamChannels: number;
upstreamChannels: number;
ofdmChannels: number;
time: string;
}

export interface DocsisChannelStatus {
ChannelID: string;
ChannelType: string;
Frequency: string;
LockStatus: string;
Modulation: string;
PowerLevel: string;
SNRLevel: string;
export function extractFirmwareVersion(html: string): string|undefined {
return swVersionMatcher.exec(html)?.groups?.swVersion
}

export function extractDocsisStatus(
Expand Down
Loading