From 0ebb4047185155b037ba035a901bc9334a7eae39 Mon Sep 17 00:00:00 2001 From: Dobroslav Totev Date: Sun, 4 Apr 2021 19:31:18 +0200 Subject: [PATCH 1/9] added technicolor modem --- src/base-command.ts | 5 ++- src/commands/discover.ts | 27 ++++++++++++++++ src/crypto.test.ts | 9 +++++- src/crypto.ts | 6 ++++ src/discovery.ts | 28 +++++++++++++++++ src/modem.ts | 45 ++++++++++++++++++++++++++ src/technicolor-modem.ts | 68 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 src/commands/discover.ts create mode 100644 src/modem.ts create mode 100644 src/technicolor-modem.ts diff --git a/src/base-command.ts b/src/base-command.ts index 2d4fe1d..f0ec20a 100644 --- a/src/base-command.ts +++ b/src/base-command.ts @@ -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) + } } diff --git a/src/commands/discover.ts b/src/commands/discover.ts new file mode 100644 index 0000000..9be16ae --- /dev/null +++ b/src/commands/discover.ts @@ -0,0 +1,27 @@ +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() { + try { + const modemIp = await discoverModemIp() + this.log(`Possibly found modem under the following IP: ${modemIp}`) + const modem = new ModemDiscovery(modemIp, this.logger) + await modem.tryTechnicolor() + } catch (error) { + this.log('Something went wrong.', error) + } + } + + async run() { + await this.discoverModem() + this.exit() + } +} diff --git a/src/crypto.test.ts b/src/crypto.test.ts index c4da716..eab72b3 100644 --- a/src/crypto.test.ts +++ b/src/crypto.test.ts @@ -1,4 +1,4 @@ -import {decrypt, deriveKey, encrypt} from './crypto' +import {decrypt, deriveKey, deriveKeyTechnicolor, encrypt} from './crypto' import {CryptoVars} from './html-parser' describe('crypto', () => { @@ -13,6 +13,13 @@ 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('encrypt', () => { expect( encrypt(testPasswordAsKey, 'textToEncrypt', cryptoVars.iv, 'authData') diff --git a/src/crypto.ts b/src/crypto.ts index 15c23b8..3d735ab 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -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, diff --git a/src/discovery.ts b/src/discovery.ts index bb1d897..134d335 100644 --- a/src/discovery.ts +++ b/src/discovery.ts @@ -1,4 +1,6 @@ import axios from 'axios' +import {Log} from './logger' +import {TechnicolorConfiguration} from './technicolor-modem' const BRIDGED_MODEM_IP = '192.168.100.1' const ROUTER_IP = '192.168.0.1' @@ -13,3 +15,29 @@ export async function discoverModemIp(): Promise { throw error } } + +export interface ModemInformation{ + deviceType: 'Arris' | 'Technicolor'; + firmwareVersion: string; +} + +export class ModemDiscovery { + constructor(private readonly modemIp: string, private readonly logger: Log) {} + + async tryTechnicolor(): Promise { + try { + const {data} = await axios.get(`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, + } + } + throw new Error('Could not determine modem type') + } catch (error) { + this.logger.warn('Could not find a router/modem under the known addresses') + throw error + } + } +} diff --git a/src/modem.ts b/src/modem.ts new file mode 100644 index 0000000..752638c --- /dev/null +++ b/src/modem.ts @@ -0,0 +1,45 @@ +import axios, {AxiosInstance} from 'axios' +import axiosCookieJarSupport from 'axios-cookiejar-support' +import {CookieJar} from 'tough-cookie' +import {Log} from './logger' + +// axios cookie support +axiosCookieJarSupport(axios) + +export interface GenericModem{ + logout(): Promise; + login(password: string): Promise; +} + +export abstract class Modem implements GenericModem { + protected readonly cookieJar: CookieJar + + protected readonly httpClient: AxiosInstance + + static USERNAME = 'admin' + + constructor(protected readonly modemIp: string, protected readonly logger: Log) { + this.cookieJar = new CookieJar() + this.httpClient = this.initAxios() + } + + login(_password: string): Promise { + throw new Error('Method not implemented.') + } + + logout(): Promise { + throw new Error('Method not implemented.') + } + + initAxios(): AxiosInstance { + return axios.create({ + withCredentials: true, + jar: this.cookieJar, + baseURL: `http://${this.modemIp}`, + headers: { + 'X-Requested-With': 'XMLHttpRequest', + }, + }) + } +} + diff --git a/src/technicolor-modem.ts b/src/technicolor-modem.ts new file mode 100644 index 0000000..5bb0317 --- /dev/null +++ b/src/technicolor-modem.ts @@ -0,0 +1,68 @@ +import {deriveKeyTechnicolor} from './crypto' +import {Log} from './logger' +import {Modem} from './modem' + +export interface TechnicolorConfiguration{ + error: string | 'ok'; + message: string; + data: { + LanMode: 'router' | 'modem'; + DeviceMode: 'Ipv4' | 'Ipv4'; + firmwareversion: string; + internetipv4: string; + AFTR: string; + IPAddressRT: string[]; + }; +} + +export interface TechnicolorSaltResponse{ + error: string | 'ok'; + salt: string; + saltwebui: string; + message?: string; +} + +export interface TechnicolorLoginResponse{ + error: 'error'; + message: string; + data: any[]; +} + +export class Technicolor extends Modem { + constructor(readonly modemIp: string, readonly logger: Log) { + super(modemIp, logger) + } + + async login(password: string) { + try { + const {data: salt} = await this.httpClient.post('/api/v1/session/login', `username=${Modem.USERNAME}&password=seeksalthash`, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}, + }) + this.logger.debug('Salt', salt) + + if (salt.message && salt.message === 'MSG_LOGIN_150') { + throw new Error('A user is already logged in') + } + + const derivedKey = deriveKeyTechnicolor(deriveKeyTechnicolor(password, salt.salt), salt.saltwebui) + const {data: loginResponse} = await this.httpClient.post('/api/v1/session/login', `username=${Modem.USERNAME}&password=${derivedKey}`, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + }, + }) + this.logger.log('fam status', loginResponse) + const {data: docsisStatus} = await this.httpClient.get('/api/v1/sta_docsis_status') + this.logger.log(`bam: ${JSON.stringify(docsisStatus)}`) + } catch (error) { + this.logger.warn(`Something went wrong with the login ${error}`) + } finally { + await this.logout() + } + } + + async logout(): Promise { + this.logger.debug('Logging out...') + return this.httpClient.post('api/v1/session/logout') + } +} From 6ef2659fb2638c971030c151aaf7491b58e26097 Mon Sep 17 00:00:00 2001 From: Dobroslav Totev Date: Mon, 5 Apr 2021 12:57:52 +0200 Subject: [PATCH 2/9] chore: updated eslintrc --- .eslintrc | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.eslintrc b/.eslintrc index e978d77..9afbedc 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,7 +1,13 @@ { "extends": [ "oclif", - "oclif-typescript" + "oclif-typescript", ], - "rules": {} + "rules": { + "no-useless-constructor": "off", + "indent": [ + "warn", + 2 + ] + } } \ No newline at end of file From 99d5f1fe782a544056219bbc86d99cd110078cc7 Mon Sep 17 00:00:00 2001 From: Dobroslav Totev Date: Mon, 5 Apr 2021 13:10:22 +0200 Subject: [PATCH 3/9] chore: cleanup --- .eslintrc | 4 +++- src/client.ts | 4 ++-- src/commands/discover.ts | 4 ++-- src/commands/docsis.ts | 6 ++++-- src/modem.ts | 4 +--- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.eslintrc b/.eslintrc index 9afbedc..45d6498 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,12 +2,14 @@ "extends": [ "oclif", "oclif-typescript", + "plugin:@typescript-eslint/recommended" ], "rules": { "no-useless-constructor": "off", "indent": [ "warn", 2 - ] + ], + "lines-between-class-members": "off" } } \ No newline at end of file diff --git a/src/client.ts b/src/client.ts index 956c23c..eb70f10 100644 --- a/src/client.ts +++ b/src/client.ts @@ -100,8 +100,8 @@ export class CliClient { cryptoVars: CryptoVars, key: string ): string { - const csrf_nonce = decrypt(key, encryptedData, cryptoVars.iv, 'nonce') - return csrf_nonce + const csrfNonce = decrypt(key, encryptedData, cryptoVars.iv, 'nonce') + return csrfNonce } async createServerRecord( diff --git a/src/commands/discover.ts b/src/commands/discover.ts index 9be16ae..868bf0c 100644 --- a/src/commands/discover.ts +++ b/src/commands/discover.ts @@ -9,7 +9,7 @@ export default class Discover extends Command { '$ vodafone-station-cli discover', ]; - async discoverModem() { + async discoverModem(): Promise { try { const modemIp = await discoverModemIp() this.log(`Possibly found modem under the following IP: ${modemIp}`) @@ -20,7 +20,7 @@ export default class Discover extends Command { } } - async run() { + async run(): Promise { await this.discoverModem() this.exit() } diff --git a/src/commands/docsis.ts b/src/commands/docsis.ts index d622e78..71553df 100644 --- a/src/commands/docsis.ts +++ b/src/commands/docsis.ts @@ -3,6 +3,7 @@ import {promises as fsp} from 'fs' import Command from '../base-command' import {CliClient} from '../client' import {discoverModemIp} from '../discovery' +import {DocsisStatus} from '../html-parser' import {OclifLogger} from '../logger' export default class Docsis extends Command { @@ -26,13 +27,14 @@ JSON data }), }; - async getDocsisStatus(password: string) { + async getDocsisStatus(password: string): Promise { const cliClient = new CliClient(await discoverModemIp(), new OclifLogger(this.log, this.warn, this.debug, this.error)) try { const csrfNonce = await cliClient.login(password) return cliClient.fetchDocsisStatus(csrfNonce) } catch (error) { this.error('Something went wrong.', error) + throw new Error('Could not fetch docsis status from modem') } finally { await cliClient.logout() } @@ -45,7 +47,7 @@ JSON data return fsp.writeFile(reportFile, data) } - async run() { + async run(): Promise { const {flags} = this.parse(Docsis) const password = process.env.VODAFONE_ROUTER_PASSWORD ?? flags.password diff --git a/src/modem.ts b/src/modem.ts index 752638c..2000790 100644 --- a/src/modem.ts +++ b/src/modem.ts @@ -13,9 +13,7 @@ export interface GenericModem{ export abstract class Modem implements GenericModem { protected readonly cookieJar: CookieJar - protected readonly httpClient: AxiosInstance - static USERNAME = 'admin' constructor(protected readonly modemIp: string, protected readonly logger: Log) { @@ -31,7 +29,7 @@ export abstract class Modem implements GenericModem { throw new Error('Method not implemented.') } - initAxios(): AxiosInstance { + private initAxios(): AxiosInstance { return axios.create({ withCredentials: true, jar: this.cookieJar, From 15d6a3f14ceab2285e829647d3c711dec69f015c Mon Sep 17 00:00:00 2001 From: Dobroslav Totev Date: Fri, 9 Apr 2021 17:30:06 +0200 Subject: [PATCH 4/9] init arris modem class --- src/arris-modem.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/arris-modem.ts diff --git a/src/arris-modem.ts b/src/arris-modem.ts new file mode 100644 index 0000000..90cdbf9 --- /dev/null +++ b/src/arris-modem.ts @@ -0,0 +1,12 @@ +import {Log} from './logger' +import {Modem} from './modem' + +export class Arris extends Modem { + constructor(readonly modemIp: string, readonly logger: Log) { + super(modemIp, logger) + } + + async logout(): Promise { + throw new Error('Not implemented!') + } +} From f2b1af124586bf1d0bbe43d06c503ef1f9ce9162 Mon Sep 17 00:00:00 2001 From: Dobroslav Totev Date: Sun, 11 Apr 2021 22:22:10 +0200 Subject: [PATCH 5/9] feat: discovery now supports both modem types --- src/arris-modem.ts | 8 +++++++- src/commands/discover.ts | 3 ++- src/discovery.ts | 44 ++++++++++++++++++++++++++++++++-------- src/html-parser.test.ts | 5 +++++ src/html-parser.ts | 5 +++++ src/technicolor-modem.ts | 2 +- 6 files changed, 56 insertions(+), 11 deletions(-) diff --git a/src/arris-modem.ts b/src/arris-modem.ts index 90cdbf9..3449a26 100644 --- a/src/arris-modem.ts +++ b/src/arris-modem.ts @@ -7,6 +7,12 @@ export class Arris extends Modem { } async logout(): Promise { - throw new Error('Not implemented!') + try { + this.logger.log('Logging out...') + return this.httpClient.post('/php/logout.php') + } catch (error) { + this.logger.error('Could not do a full session logout', error) + throw error + } } } diff --git a/src/commands/discover.ts b/src/commands/discover.ts index 868bf0c..5196146 100644 --- a/src/commands/discover.ts +++ b/src/commands/discover.ts @@ -14,7 +14,8 @@ export default class Discover extends Command { const modemIp = await discoverModemIp() this.log(`Possibly found modem under the following IP: ${modemIp}`) const modem = new ModemDiscovery(modemIp, this.logger) - await modem.tryTechnicolor() + const discoveredModem = await modem.discover() + this.log(`Discovered modem: ${JSON.stringify(discoveredModem)}`) } catch (error) { this.log('Something went wrong.', error) } diff --git a/src/discovery.ts b/src/discovery.ts index 134d335..89ddd00 100644 --- a/src/discovery.ts +++ b/src/discovery.ts @@ -1,4 +1,5 @@ import axios from 'axios' +import {extractFirmwareVersion} from './html-parser' import {Log} from './logger' import {TechnicolorConfiguration} from './technicolor-modem' const BRIDGED_MODEM_IP = '192.168.100.1' @@ -19,22 +20,49 @@ export async function discoverModemIp(): Promise { 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 { + const {data} = await axios.get(`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 { + 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 { try { - const {data} = await axios.get(`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, - } + const discovery = await Promise.allSettled([this.tryArris(), this.tryTechnicolor()]) + const maybeModem = discovery.find(fam => fam.status === 'fulfilled') as PromiseFulfilledResult | undefined + if (!maybeModem) { + throw new Error('Modem discovery was unsuccessful') } - throw new Error('Could not determine modem type') + return maybeModem.value } catch (error) { this.logger.warn('Could not find a router/modem under the known addresses') throw error diff --git a/src/html-parser.test.ts b/src/html-parser.test.ts index cf1341d..01bf89b 100644 --- a/src/html-parser.test.ts +++ b/src/html-parser.test.ts @@ -4,6 +4,7 @@ import { extractCredentialString, extractCryptoVars, extractDocsisStatus, + extractFirmwareVersion, } from './html-parser' describe('htmlParser', () => { @@ -44,4 +45,8 @@ describe('htmlParser', () => { 'someRandomCatchyHash37f1f79255b66b5c02348e3dc6ff5fcd559654e2' ) }) + + test('extractFirmwareVersion', () => { + expect(extractFirmwareVersion(fixtureIndex)).toBe('01.02.068.11.EURO.PC20') + }) }) diff --git a/src/html-parser.ts b/src/html-parser.ts index b0d280e..a96141b 100644 --- a/src/html-parser.ts +++ b/src/html-parser.ts @@ -2,6 +2,7 @@ const nonceMatcher = /var csp_nonce = "(?.*?)";/gm const ivMatcher = /var myIv = ["|'](?.*?)["|'];/gm const saltMatcher = /var mySalt = ["|'](?.*?)["|'];/gm const sessionIdMatcher = /var currentSessionId = ["|'](?.*?)["|'];/gm +const swVersionMatcher = /_ga.swVersion = ["|'](?.*?)["|'];/gm export interface CryptoVars { nonce: string; @@ -18,6 +19,10 @@ export function extractCryptoVars(html: string): CryptoVars { return {nonce, iv, salt, sessionId} as CryptoVars } +export function extractFirmwareVersion(html: string): string|undefined { + return swVersionMatcher.exec(html)?.groups?.swVersion +} + export interface DocsisStatus { downstream: DocsisChannelStatus[]; upstream: DocsisChannelStatus[]; diff --git a/src/technicolor-modem.ts b/src/technicolor-modem.ts index 5bb0317..b834209 100644 --- a/src/technicolor-modem.ts +++ b/src/technicolor-modem.ts @@ -33,7 +33,7 @@ export class Technicolor extends Modem { super(modemIp, logger) } - async login(password: string) { + async login(password: string): Promise { try { const {data: salt} = await this.httpClient.post('/api/v1/session/login', `username=${Modem.USERNAME}&password=seeksalthash`, { headers: { From 6f92c25d72df8e4a0eb7ad446e5a5321e430834d Mon Sep 17 00:00:00 2001 From: Dobroslav Totev Date: Mon, 12 Apr 2021 07:47:42 +0200 Subject: [PATCH 6/9] refactoring: extracted arris modem --- src/arris-modem.ts | 18 --- src/client.ts | 135 +----------------- src/commands/docsis.ts | 17 +-- src/discovery.ts | 6 +- src/html-parser.ts | 21 +-- src/modem.ts | 25 +++- .../arris-modem.test.ts} | 12 +- src/modem/arris-modem.ts | 134 +++++++++++++++++ src/modem/factory.ts | 16 +++ src/{ => modem}/technicolor-modem.ts | 13 +- 10 files changed, 203 insertions(+), 194 deletions(-) delete mode 100644 src/arris-modem.ts rename src/{client.test.ts => modem/arris-modem.test.ts} (64%) create mode 100644 src/modem/arris-modem.ts create mode 100644 src/modem/factory.ts rename src/{ => modem}/technicolor-modem.ts (87%) diff --git a/src/arris-modem.ts b/src/arris-modem.ts deleted file mode 100644 index 3449a26..0000000 --- a/src/arris-modem.ts +++ /dev/null @@ -1,18 +0,0 @@ -import {Log} from './logger' -import {Modem} from './modem' - -export class Arris extends Modem { - constructor(readonly modemIp: string, readonly logger: Log) { - super(modemIp, logger) - } - - async logout(): Promise { - try { - this.logger.log('Logging out...') - return this.httpClient.post('/php/logout.php') - } catch (error) { - this.logger.error('Could not do a full session logout', error) - throw error - } - } -} diff --git a/src/client.ts b/src/client.ts index eb70f10..57a8695 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,21 +1,11 @@ import axios, {AxiosInstance} from 'axios' import axiosCookieJarSupport from 'axios-cookiejar-support' import {CookieJar} from 'tough-cookie' -import {decrypt, deriveKey, encrypt} from './crypto' -import { - CryptoVars, - DocsisStatus, - extractCredentialString, - extractCryptoVars, - extractDocsisStatus, -} from './html-parser' import {Log} from './logger' // axios cookie support axiosCookieJarSupport(axios) -const USERNAME = 'admin' - export interface SetPasswordRequest { AuthData: string; EncryptData: string; @@ -46,118 +36,7 @@ export class CliClient { }) } - async login(password: string) { - const cryptoVars = await this.getCurrentCryptoVars() - const encPw = this.encryptPassword(password, cryptoVars) - this.logger.debug('Encrypted password: ', encPw) - const serverSetPassword = await this.createServerRecord(encPw) - this.logger.debug('ServerSetPassword: ', serverSetPassword) - - const csrfNonce = this.loginPasswordCheck( - serverSetPassword.encryptData, - cryptoVars, - deriveKey(password, cryptoVars.salt) - ) - this.logger.debug('Csrf nonce: ', csrfNonce) - - await this.addCredentialToCookie() - return csrfNonce - } - - async getCurrentCryptoVars(): Promise { - try { - const {data} = await this.httpClient.get('/', { - headers: {Accept: 'text/html,application/xhtml+xml,application/xml'}, - }) - const cryptoVars = extractCryptoVars(data) - this.logger.debug('Parsed crypto vars: ', cryptoVars) - return cryptoVars - } catch (error) { - this.logger.error('Could not get the index page from the router', error) - throw error - } - } - - encryptPassword( - password: string, - cryptoVars: CryptoVars - ): SetPasswordRequest { - const jsData = - '{"Password": "' + password + '", "Nonce": "' + cryptoVars.sessionId + '"}' - const key = deriveKey(password, cryptoVars.salt) - const authData = 'loginPassword' - const encryptData = encrypt(key, jsData, cryptoVars.iv, authData) - - return { - EncryptData: encryptData, - Name: USERNAME, - AuthData: authData, - } - } - - loginPasswordCheck( - encryptedData: string, - cryptoVars: CryptoVars, - key: string - ): string { - const csrfNonce = decrypt(key, encryptedData, cryptoVars.iv, 'nonce') - return csrfNonce - } - - async createServerRecord( - setPasswordRequest: SetPasswordRequest - ): Promise { - try { - const {data} = await this.httpClient.post( - '/php/ajaxSet_Password.php', - setPasswordRequest - ) - // TODO handle wrong password case - // { p_status: 'Lockout', p_waitTime: 1 } - return data - } catch (error) { - this.logger.error('Could not set password on remote router.', error) - throw error - } - } - - async addCredentialToCookie() { - const credential = await this.fetchCredential() - this.logger.debug('Credential: ', credential) - // set obligatory static cookie - this.cookieJar.setCookie(`credential= ${credential}`, `http://${this.modemIp}`) - } - - async fetchCredential(): Promise { - try { - const {data} = await this.httpClient.get('/base_95x.js') - return extractCredentialString(data) - } catch (error) { - this.logger.error('Could not fetch credential.', error) - throw error - } - } - - async fetchDocsisStatus( - csrfNonce: string - ): Promise { - try { - const {data} = await this.httpClient.get('/php/status_docsis_data.php', { - headers: { - csrfNonce, - Referer: `http://${this.modemIp}/?status_docsis&mid=StatusDocsis`, - 'X-Requested-With': 'XMLHttpRequest', - Connection: 'keep-alive', - }, - }) - return extractDocsisStatus(data) - } catch (error) { - this.logger.error('Could not fetch remote docsis status', error) - throw error - } - } - - async restart(csrfNonce: string) { + async restart(csrfNonce: string): Promise { try { const {data} = await this.httpClient.post( 'php/ajaxSet_status_restart.php', @@ -180,16 +59,4 @@ export class CliClient { throw error } } - - async logout(): Promise { - try { - this.logger.log('Logging out...') - await this.httpClient.post('/php/logout.php') - return true - } catch (error) { - this.logger.error('Could not do a full session logout', error) - throw error - } - } } - diff --git a/src/commands/docsis.ts b/src/commands/docsis.ts index 9e22cee..ed17dfc 100644 --- a/src/commands/docsis.ts +++ b/src/commands/docsis.ts @@ -1,10 +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 {DocsisStatus} from '../html-parser' -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 = @@ -28,15 +27,17 @@ JSON data }; async getDocsisStatus(password: string): Promise { - const cliClient = new CliClient(await discoverModemIp(), new OclifLogger(this.log, this.warn, this.debug, this.error)) + 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) + return modem.docsis() } 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() } } diff --git a/src/discovery.ts b/src/discovery.ts index 89ddd00..3555a0c 100644 --- a/src/discovery.ts +++ b/src/discovery.ts @@ -1,7 +1,7 @@ import axios from 'axios' import {extractFirmwareVersion} from './html-parser' import {Log} from './logger' -import {TechnicolorConfiguration} from './technicolor-modem' +import {TechnicolorConfiguration} from './modem/technicolor-modem' const BRIDGED_MODEM_IP = '192.168.100.1' const ROUTER_IP = '192.168.0.1' @@ -55,10 +55,10 @@ export class ModemDiscovery { } } - async discover(): Promise { + async discover(): Promise { try { const discovery = await Promise.allSettled([this.tryArris(), this.tryTechnicolor()]) - const maybeModem = discovery.find(fam => fam.status === 'fulfilled') as PromiseFulfilledResult | undefined + const maybeModem = discovery.find(fam => fam.status === 'fulfilled') as PromiseFulfilledResult | undefined if (!maybeModem) { throw new Error('Modem discovery was unsuccessful') } diff --git a/src/html-parser.ts b/src/html-parser.ts index a96141b..95df948 100644 --- a/src/html-parser.ts +++ b/src/html-parser.ts @@ -1,3 +1,5 @@ +import {DocsisChannelStatus, DocsisStatus} from './modem' + const nonceMatcher = /var csp_nonce = "(?.*?)";/gm const ivMatcher = /var myIv = ["|'](?.*?)["|'];/gm const saltMatcher = /var mySalt = ["|'](?.*?)["|'];/gm @@ -23,25 +25,6 @@ export function extractFirmwareVersion(html: string): string|undefined { return swVersionMatcher.exec(html)?.groups?.swVersion } -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 extractDocsisStatus( html: string, date: Date = new Date() diff --git a/src/modem.ts b/src/modem.ts index 2000790..d605df5 100644 --- a/src/modem.ts +++ b/src/modem.ts @@ -2,13 +2,32 @@ import axios, {AxiosInstance} from 'axios' import axiosCookieJarSupport from 'axios-cookiejar-support' import {CookieJar} from 'tough-cookie' import {Log} from './logger' - // axios cookie support axiosCookieJarSupport(axios) +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 interface GenericModem{ logout(): Promise; login(password: string): Promise; + docsis(): Promise; } export abstract class Modem implements GenericModem { @@ -21,6 +40,10 @@ export abstract class Modem implements GenericModem { this.httpClient = this.initAxios() } + docsis(): Promise { + throw new Error('Method not implemented.') + } + login(_password: string): Promise { throw new Error('Method not implemented.') } diff --git a/src/client.test.ts b/src/modem/arris-modem.test.ts similarity index 64% rename from src/client.test.ts rename to src/modem/arris-modem.test.ts index 39e3b0d..9fdb4b3 100644 --- a/src/client.test.ts +++ b/src/modem/arris-modem.test.ts @@ -1,8 +1,8 @@ -import {CliClient} from './client' -import {CryptoVars} from './html-parser' -import {ConsoleLogger} from './logger' +import {CryptoVars} from '../html-parser' +import {ConsoleLogger} from '../logger' +import {Arris} from './arris-modem' -describe('client', () => { +describe('Arris', () => { test('should encrypt', () => { const expected = { EncryptData: @@ -16,7 +16,7 @@ describe('client', () => { sessionId: '01a91cedd129fd8c6f18e3a1b58d096f', nonce: 'WslSZgE7NuQr+1BMqiYEOBMzQlo=', } - const cliClient = new CliClient('0.0.0.0', new ConsoleLogger()) - expect(cliClient.encryptPassword('test', given)).toEqual(expected) + const arrisModem = new Arris('0.0.0.0', new ConsoleLogger()) + expect(arrisModem.encryptPassword('test', given)).toEqual(expected) }) }) diff --git a/src/modem/arris-modem.ts b/src/modem/arris-modem.ts new file mode 100644 index 0000000..ec43f33 --- /dev/null +++ b/src/modem/arris-modem.ts @@ -0,0 +1,134 @@ +import {SetPasswordRequest, SetPasswordResponse} from '../client' +import {decrypt, deriveKey, encrypt} from '../crypto' +import {CryptoVars, extractCredentialString, extractCryptoVars, extractDocsisStatus} from '../html-parser' +import {Log} from '../logger' +import {DocsisStatus, Modem} from '../modem' + +export class Arris extends Modem { + private csrfNonce = '' + constructor(readonly modemIp: string, readonly logger: Log) { + super(modemIp, logger) + } + + async logout(): Promise { + try { + this.logger.log('Logging out...') + return this.httpClient.post('/php/logout.php') + } catch (error) { + this.logger.error('Could not do a full session logout', error) + throw error + } + } + + async login(password: string): Promise { + const cryptoVars = await this.getCurrentCryptoVars() + const encPw = this.encryptPassword(password, cryptoVars) + this.logger.debug('Encrypted password: ', encPw) + const serverSetPassword = await this.createServerRecord(encPw) + this.logger.debug('ServerSetPassword: ', serverSetPassword) + + const csrfNonce = this.loginPasswordCheck( + serverSetPassword.encryptData, + cryptoVars, + deriveKey(password, cryptoVars.salt) + ) + this.logger.debug('Csrf nonce: ', csrfNonce) + + await this.addCredentialToCookie() + this.csrfNonce = csrfNonce + } + + async getCurrentCryptoVars(): Promise { + try { + const {data} = await this.httpClient.get('/', { + headers: {Accept: 'text/html,application/xhtml+xml,application/xml'}, + }) + const cryptoVars = extractCryptoVars(data) + this.logger.debug('Parsed crypto vars: ', cryptoVars) + return cryptoVars + } catch (error) { + this.logger.error('Could not get the index page from the router', error) + throw error + } + } + + encryptPassword( + password: string, + cryptoVars: CryptoVars + ): SetPasswordRequest { + const jsData = + '{"Password": "' + password + '", "Nonce": "' + cryptoVars.sessionId + '"}' + const key = deriveKey(password, cryptoVars.salt) + const authData = 'loginPassword' + const encryptData = encrypt(key, jsData, cryptoVars.iv, authData) + + return { + EncryptData: encryptData, + Name: Modem.USERNAME, + AuthData: authData, + } + } + + loginPasswordCheck( + encryptedData: string, + cryptoVars: CryptoVars, + key: string + ): string { + const csrfNonce = decrypt(key, encryptedData, cryptoVars.iv, 'nonce') + return csrfNonce + } + + async createServerRecord( + setPasswordRequest: SetPasswordRequest + ): Promise { + try { + const {data} = await this.httpClient.post( + '/php/ajaxSet_Password.php', + setPasswordRequest + ) + // TODO handle wrong password case + // { p_status: 'Lockout', p_waitTime: 1 } + return data + } catch (error) { + this.logger.error('Could not set password on remote router.', error) + throw error + } + } + + async addCredentialToCookie(): Promise { + const credential = await this.fetchCredential() + this.logger.debug('Credential: ', credential) + // set obligatory static cookie + this.cookieJar.setCookie(`credential= ${credential}`, `http://${this.modemIp}`) + } + + async fetchCredential(): Promise { + try { + const {data} = await this.httpClient.get('/base_95x.js') + return extractCredentialString(data) + } catch (error) { + this.logger.error('Could not fetch credential.', error) + throw error + } + } + + async docsis(): Promise { + if (!this.csrfNonce) { + throw new Error('A valid csrfNonce is required in order to query the modem.') + } + try { + const {data} = await this.httpClient.get('/php/status_docsis_data.php', { + headers: { + csrfNonce: this.csrfNonce, + Referer: `http://${this.modemIp}/?status_docsis&mid=StatusDocsis`, + 'X-Requested-With': 'XMLHttpRequest', + Connection: 'keep-alive', + }, + }) + return extractDocsisStatus(data) + } catch (error) { + this.logger.error('Could not fetch remote docsis status', error) + throw error + } + } +} diff --git a/src/modem/factory.ts b/src/modem/factory.ts new file mode 100644 index 0000000..5913108 --- /dev/null +++ b/src/modem/factory.ts @@ -0,0 +1,16 @@ +import {Arris} from './arris-modem' +import {ModemInformation} from '../discovery' +import {ConsoleLogger, Log} from '../logger' +import {Modem} from '../modem' +import {Technicolor} from './technicolor-modem' + +export function modemFactory(modemInfo: ModemInformation, logger: Log = new ConsoleLogger()): Modem { + switch (modemInfo.deviceType) { + case 'Arris': + return new Arris(modemInfo.ipAddress, logger) + case 'Technicolor': + return new Technicolor(modemInfo.ipAddress, logger) + default: + throw new Error(`Unsupported modem ${modemInfo.deviceType}`) + } +} diff --git a/src/technicolor-modem.ts b/src/modem/technicolor-modem.ts similarity index 87% rename from src/technicolor-modem.ts rename to src/modem/technicolor-modem.ts index b834209..50b14ed 100644 --- a/src/technicolor-modem.ts +++ b/src/modem/technicolor-modem.ts @@ -1,6 +1,6 @@ -import {deriveKeyTechnicolor} from './crypto' -import {Log} from './logger' -import {Modem} from './modem' +import {deriveKeyTechnicolor} from '../crypto' +import {Log} from '../logger' +import {DocsisStatus, Modem} from '../modem' export interface TechnicolorConfiguration{ error: string | 'ok'; @@ -52,8 +52,6 @@ export class Technicolor extends Modem { }, }) this.logger.log('fam status', loginResponse) - const {data: docsisStatus} = await this.httpClient.get('/api/v1/sta_docsis_status') - this.logger.log(`bam: ${JSON.stringify(docsisStatus)}`) } catch (error) { this.logger.warn(`Something went wrong with the login ${error}`) } finally { @@ -61,6 +59,11 @@ export class Technicolor extends Modem { } } + async docsis(): Promise { + const {data: docsisStatus} = await this.httpClient.get('/api/v1/sta_docsis_status') + return docsisStatus + } + async logout(): Promise { this.logger.debug('Logging out...') return this.httpClient.post('api/v1/session/logout') From 71ef11c96a0024de26a37cd24d4af1d9a346cf87 Mon Sep 17 00:00:00 2001 From: Dobroslav Totev Date: Fri, 16 Apr 2021 12:23:35 +0200 Subject: [PATCH 7/9] refactoring: moved out client logic to arris-modem --- src/client.ts | 62 ---------------------------------------- src/commands/restart.ts | 19 ++++++------ src/modem.ts | 4 +++ src/modem/arris-modem.ts | 37 +++++++++++++++++++++++- 4 files changed, 50 insertions(+), 72 deletions(-) delete mode 100644 src/client.ts diff --git a/src/client.ts b/src/client.ts deleted file mode 100644 index 57a8695..0000000 --- a/src/client.ts +++ /dev/null @@ -1,62 +0,0 @@ -import axios, {AxiosInstance} from 'axios' -import axiosCookieJarSupport from 'axios-cookiejar-support' -import {CookieJar} from 'tough-cookie' -import {Log} from './logger' - -// axios cookie support -axiosCookieJarSupport(axios) - -export interface SetPasswordRequest { - AuthData: string; - EncryptData: string; - Name: string; -} - -export interface SetPasswordResponse { - p_status: string; - encryptData: string; - p_waitTime?: number; -} - -export class CliClient { - private readonly cookieJar: CookieJar - - private readonly httpClient: AxiosInstance - - constructor(private readonly modemIp: string, private readonly logger: Log) { - this.cookieJar = new CookieJar() - this.httpClient = this.initAxios() - } - - initAxios(): AxiosInstance { - return axios.create({ - withCredentials: true, - jar: this.cookieJar, - baseURL: `http://${this.modemIp}`, - }) - } - - async restart(csrfNonce: string): Promise { - try { - const {data} = await this.httpClient.post( - 'php/ajaxSet_status_restart.php', - { - RestartReset: 'Restart', - }, - { - headers: { - csrfNonce, - Referer: `http://${this.modemIp}/?status_docsis&mid=StatusDocsis`, - 'X-Requested-With': 'XMLHttpRequest', - Connection: 'keep-alive', - }, - } - ) - this.logger.log('Router is restarting') - return data - } catch (error) { - this.logger.error('Could not restart router.', error) - throw error - } - } -} diff --git a/src/commands/restart.ts b/src/commands/restart.ts index d800ee8..98b3bc4 100644 --- a/src/commands/restart.ts +++ b/src/commands/restart.ts @@ -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 = @@ -19,19 +18,21 @@ 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 { + 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) + return modem.restart() } catch (error) { this.log('Something went wrong.', error) } finally { - await cliClient.logout() + await modem.logout() } } - async run() { + async run(): Promise { const {flags} = this.parse(Restart) const password = process.env.VODAFONE_ROUTER_PASSWORD ?? flags.password diff --git a/src/modem.ts b/src/modem.ts index d605df5..ad165ef 100644 --- a/src/modem.ts +++ b/src/modem.ts @@ -28,6 +28,7 @@ export interface GenericModem{ logout(): Promise; login(password: string): Promise; docsis(): Promise; + restart(): Promise; } export abstract class Modem implements GenericModem { @@ -39,6 +40,9 @@ export abstract class Modem implements GenericModem { this.cookieJar = new CookieJar() this.httpClient = this.initAxios() } + restart(): Promise { + throw new Error('Method not implemented.') + } docsis(): Promise { throw new Error('Method not implemented.') diff --git a/src/modem/arris-modem.ts b/src/modem/arris-modem.ts index ec43f33..12d53b2 100644 --- a/src/modem/arris-modem.ts +++ b/src/modem/arris-modem.ts @@ -1,9 +1,20 @@ -import {SetPasswordRequest, SetPasswordResponse} from '../client' import {decrypt, deriveKey, encrypt} from '../crypto' import {CryptoVars, extractCredentialString, extractCryptoVars, extractDocsisStatus} from '../html-parser' import {Log} from '../logger' import {DocsisStatus, Modem} from '../modem' +export interface SetPasswordRequest { + AuthData: string; + EncryptData: string; + Name: string; +} + +export interface SetPasswordResponse { + p_status: string; + encryptData: string; + p_waitTime?: number; +} + export class Arris extends Modem { private csrfNonce = '' constructor(readonly modemIp: string, readonly logger: Log) { @@ -131,4 +142,28 @@ export class Arris extends Modem { throw error } } + + async restart(): Promise { + try { + const {data} = await this.httpClient.post( + 'php/ajaxSet_status_restart.php', + { + RestartReset: 'Restart', + }, + { + headers: { + csrfNonce: this.csrfNonce, + Referer: `http://${this.modemIp}/?status_docsis&mid=StatusDocsis`, + 'X-Requested-With': 'XMLHttpRequest', + Connection: 'keep-alive', + }, + } + ) + this.logger.log('Router is restarting') + return data + } catch (error) { + this.logger.error('Could not restart router.', error) + throw error + } + } } From 9033903bfcd38a4b636c169f16342b39318c6660 Mon Sep 17 00:00:00 2001 From: Dobroslav Totev Date: Fri, 16 Apr 2021 16:10:27 +0200 Subject: [PATCH 8/9] feat: add technicolor restart --- README.md | 5 ++-- package.json | 2 +- src/commands/docsis.ts | 5 ++-- src/commands/restart.ts | 7 ++--- src/crypto.test.ts | 9 +++++++ src/modem/technicolor-modem.ts | 48 +++++++++++++++++++++++----------- 6 files changed, 53 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 19f2e65..12ce9bf 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 diff --git a/package.json b/package.json index 62e4379..2b74498 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vodafone-station-cli", - "description": "Access your Arris TG3442DE (aka Vodafone Station) from the comfort of the command line.", + "description": "Access your Vodafone Station from the comfort of the command line.", "version": "1.0.0", "author": "Dobroslav Totev", "bin": { diff --git a/src/commands/docsis.ts b/src/commands/docsis.ts index ed17dfc..e09ebf6 100644 --- a/src/commands/docsis.ts +++ b/src/commands/docsis.ts @@ -32,7 +32,8 @@ JSON data const modem = modemFactory(discoveredModem) try { await modem.login(password) - return modem.docsis() + 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') @@ -51,7 +52,7 @@ JSON data async run(): Promise { 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' diff --git a/src/commands/restart.ts b/src/commands/restart.ts index 98b3bc4..257e327 100644 --- a/src/commands/restart.ts +++ b/src/commands/restart.ts @@ -24,7 +24,8 @@ export default class Restart extends Command { const modem = modemFactory(discoveredModem) try { await modem.login(password) - return modem.restart() + const restart = await modem.restart() + return restart } catch (error) { this.log('Something went wrong.', error) } finally { @@ -35,14 +36,14 @@ export default class Restart extends Command { async run(): Promise { 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() } diff --git a/src/crypto.test.ts b/src/crypto.test.ts index eab72b3..36f897e 100644 --- a/src/crypto.test.ts +++ b/src/crypto.test.ts @@ -20,6 +20,15 @@ describe('crypto', () => { 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') diff --git a/src/modem/technicolor-modem.ts b/src/modem/technicolor-modem.ts index 50b14ed..139a11e 100644 --- a/src/modem/technicolor-modem.ts +++ b/src/modem/technicolor-modem.ts @@ -2,9 +2,13 @@ import {deriveKeyTechnicolor} from '../crypto' import {Log} from '../logger' import {DocsisStatus, Modem} from '../modem' -export interface TechnicolorConfiguration{ - error: string | 'ok'; - message: string; +export interface TechnicolorBaseResponse{ + error: string | 'ok' | 'error'; + message: string; + data?: {[key: string]: unknown}; +} + +export interface TechnicolorConfiguration extends TechnicolorBaseResponse{ data: { LanMode: 'router' | 'modem'; DeviceMode: 'Ipv4' | 'Ipv4'; @@ -15,17 +19,13 @@ export interface TechnicolorConfiguration{ }; } -export interface TechnicolorSaltResponse{ - error: string | 'ok'; +export interface TechnicolorSaltResponse extends TechnicolorBaseResponse{ salt: string; saltwebui: string; - message?: string; } -export interface TechnicolorLoginResponse{ - error: 'error'; - message: string; - data: any[]; +export interface TechnicolorTokenResponse extends TechnicolorBaseResponse{ + token: string; } export class Technicolor extends Modem { @@ -46,16 +46,15 @@ export class Technicolor extends Modem { } const derivedKey = deriveKeyTechnicolor(deriveKeyTechnicolor(password, salt.salt), salt.saltwebui) - const {data: loginResponse} = await this.httpClient.post('/api/v1/session/login', `username=${Modem.USERNAME}&password=${derivedKey}`, { + this.logger.debug('Derived key', derivedKey) + const {data: loginResponse} = await this.httpClient.post('/api/v1/session/login', `username=${Modem.USERNAME}&password=${derivedKey}`, { headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', }, }) - this.logger.log('fam status', loginResponse) + this.logger.debug('Login status', loginResponse) } catch (error) { this.logger.warn(`Something went wrong with the login ${error}`) - } finally { - await this.logout() } } @@ -65,7 +64,26 @@ export class Technicolor extends Modem { } async logout(): Promise { - this.logger.debug('Logging out...') + this.logger.debug('Logging outB...') return this.httpClient.post('api/v1/session/logout') } + + async restart(): Promise { + const {data: tokenResponse} = await this.httpClient.get('api/v1/session/init_page') + this.logger.debug('Token response: ', tokenResponse) + const {data: restartResponse} = await this.httpClient.post('api/v1/sta_restart', + 'restart=Router%2CWifi%2CVoIP%2CDect%2CMoCA', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'X-CSRF-TOKEN': tokenResponse.token, + }, + }) + + if (restartResponse?.error === 'error') { + this.logger.debug(restartResponse) + throw new Error(`Could not restart router: ${restartResponse.message}`) + } + return restartResponse + } } + From aef225861a73f7188a9ddbfd3662391b50d2f381 Mon Sep 17 00:00:00 2001 From: Dobroslav Totev Date: Fri, 16 Apr 2021 16:54:33 +0200 Subject: [PATCH 9/9] chore: version bump --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2b74498..2eab8ea 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vodafone-station-cli", "description": "Access your Vodafone Station from the comfort of the command line.", - "version": "1.0.0", + "version": "1.1.0", "author": "Dobroslav Totev", "bin": { "vodafone-station-cli": "./bin/run" @@ -66,4 +66,4 @@ "lint": "eslint . --ext .ts --config .eslintrc" }, "types": "lib/index.d.ts" -} +} \ No newline at end of file