Skip to content

Commit

Permalink
feat: add support for cga4233de (#2)
Browse files Browse the repository at this point in the history
* added technicolor modem

* chore: updated eslintrc

* chore: cleanup

* init arris modem class

* feat: discovery now supports both modem types

* refactoring: extracted arris modem

* refactoring: moved out client logic to arris-modem

* feat: add technicolor restart

* chore: version bump
  • Loading branch information
totev committed Apr 16, 2021
1 parent a0f4725 commit 8654187
Show file tree
Hide file tree
Showing 17 changed files with 374 additions and 108 deletions.
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

0 comments on commit 8654187

Please sign in to comment.