From 8654187c1347e945ba0ff5deadc3c13a6270b0fe Mon Sep 17 00:00:00 2001 From: Dobroslav Totev Date: Fri, 16 Apr 2021 17:14:01 +0200 Subject: [PATCH] feat: add support for cga4233de (#2) * 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 --- .eslintrc | 12 ++- README.md | 5 +- package.json | 6 +- src/base-command.ts | 5 +- src/commands/discover.ts | 28 ++++++ src/commands/docsis.ts | 24 ++--- src/commands/restart.ts | 24 ++--- src/crypto.test.ts | 18 +++- src/crypto.ts | 6 ++ src/discovery.ts | 56 ++++++++++++ src/html-parser.test.ts | 5 ++ src/html-parser.ts | 22 ++--- src/modem.ts | 70 +++++++++++++++ .../arris-modem.test.ts} | 12 +-- src/{client.ts => modem/arris-modem.ts} | 84 ++++++----------- src/modem/factory.ts | 16 ++++ src/modem/technicolor-modem.ts | 89 +++++++++++++++++++ 17 files changed, 374 insertions(+), 108 deletions(-) create mode 100644 src/commands/discover.ts create mode 100644 src/modem.ts rename src/{client.test.ts => modem/arris-modem.test.ts} (64%) rename src/{client.ts => modem/arris-modem.ts} (74%) create mode 100644 src/modem/factory.ts create mode 100644 src/modem/technicolor-modem.ts diff --git a/.eslintrc b/.eslintrc index e978d77..45d6498 100644 --- a/.eslintrc +++ b/.eslintrc @@ -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" + } } \ No newline at end of file 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..2eab8ea 100644 --- a/package.json +++ b/package.json @@ -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" @@ -66,4 +66,4 @@ "lint": "eslint . --ext .ts --config .eslintrc" }, "types": "lib/index.d.ts" -} +} \ No newline at end of file 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..5196146 --- /dev/null +++ b/src/commands/discover.ts @@ -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 { + 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 { + await this.discoverModem() + this.exit() + } +} diff --git a/src/commands/docsis.ts b/src/commands/docsis.ts index 419aa47..e09ebf6 100644 --- a/src/commands/docsis.ts +++ b/src/commands/docsis.ts @@ -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 = @@ -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 { + 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() } } @@ -45,10 +49,10 @@ 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 + 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 d800ee8..257e327 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,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 { + 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 { 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 c4da716..36f897e 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,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') 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..3555a0c 100644 --- a/src/discovery.ts +++ b/src/discovery.ts @@ -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' @@ -13,3 +16,56 @@ export async function discoverModemIp(): Promise { 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 { + 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 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') + } + 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..95df948 100644 --- a/src/html-parser.ts +++ b/src/html-parser.ts @@ -1,7 +1,10 @@ +import {DocsisChannelStatus, DocsisStatus} from './modem' + 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,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( diff --git a/src/modem.ts b/src/modem.ts new file mode 100644 index 0000000..ad165ef --- /dev/null +++ b/src/modem.ts @@ -0,0 +1,70 @@ +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; + restart(): 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() + } + restart(): Promise { + throw new Error('Method not implemented.') + } + + docsis(): Promise { + throw new Error('Method not implemented.') + } + + login(_password: string): Promise { + throw new Error('Method not implemented.') + } + + logout(): Promise { + throw new Error('Method not implemented.') + } + + private initAxios(): AxiosInstance { + return axios.create({ + withCredentials: true, + jar: this.cookieJar, + baseURL: `http://${this.modemIp}`, + headers: { + 'X-Requested-With': 'XMLHttpRequest', + }, + }) + } +} + 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/client.ts b/src/modem/arris-modem.ts similarity index 74% rename from src/client.ts rename to src/modem/arris-modem.ts index 956c23c..12d53b2 100644 --- a/src/client.ts +++ b/src/modem/arris-modem.ts @@ -1,20 +1,7 @@ -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' +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; @@ -28,25 +15,23 @@ export interface SetPasswordResponse { 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() +export class Arris extends Modem { + private csrfNonce = '' + constructor(readonly modemIp: string, readonly logger: Log) { + super(modemIp, logger) } - initAxios(): AxiosInstance { - return axios.create({ - withCredentials: true, - jar: this.cookieJar, - baseURL: `http://${this.modemIp}`, - }) + 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) { + async login(password: string): Promise { const cryptoVars = await this.getCurrentCryptoVars() const encPw = this.encryptPassword(password, cryptoVars) this.logger.debug('Encrypted password: ', encPw) @@ -61,7 +46,7 @@ export class CliClient { this.logger.debug('Csrf nonce: ', csrfNonce) await this.addCredentialToCookie() - return csrfNonce + this.csrfNonce = csrfNonce } async getCurrentCryptoVars(): Promise { @@ -90,7 +75,7 @@ export class CliClient { return { EncryptData: encryptData, - Name: USERNAME, + Name: Modem.USERNAME, AuthData: authData, } } @@ -100,8 +85,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( @@ -121,7 +106,7 @@ export class CliClient { } } - async addCredentialToCookie() { + async addCredentialToCookie(): Promise { const credential = await this.fetchCredential() this.logger.debug('Credential: ', credential) // set obligatory static cookie @@ -138,13 +123,14 @@ export class CliClient { } } - async fetchDocsisStatus( - csrfNonce: string - ): Promise { + 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, + csrfNonce: this.csrfNonce, Referer: `http://${this.modemIp}/?status_docsis&mid=StatusDocsis`, 'X-Requested-With': 'XMLHttpRequest', Connection: 'keep-alive', @@ -157,7 +143,7 @@ export class CliClient { } } - async restart(csrfNonce: string) { + async restart(): Promise { try { const {data} = await this.httpClient.post( 'php/ajaxSet_status_restart.php', @@ -166,7 +152,7 @@ export class CliClient { }, { headers: { - csrfNonce, + csrfNonce: this.csrfNonce, Referer: `http://${this.modemIp}/?status_docsis&mid=StatusDocsis`, 'X-Requested-With': 'XMLHttpRequest', Connection: 'keep-alive', @@ -180,16 +166,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/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/modem/technicolor-modem.ts b/src/modem/technicolor-modem.ts new file mode 100644 index 0000000..139a11e --- /dev/null +++ b/src/modem/technicolor-modem.ts @@ -0,0 +1,89 @@ +import {deriveKeyTechnicolor} from '../crypto' +import {Log} from '../logger' +import {DocsisStatus, Modem} from '../modem' + +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'; + firmwareversion: string; + internetipv4: string; + AFTR: string; + IPAddressRT: string[]; + }; +} + +export interface TechnicolorSaltResponse extends TechnicolorBaseResponse{ + salt: string; + saltwebui: string; +} + +export interface TechnicolorTokenResponse extends TechnicolorBaseResponse{ + token: string; +} + +export class Technicolor extends Modem { + constructor(readonly modemIp: string, readonly logger: Log) { + super(modemIp, logger) + } + + async login(password: string): Promise { + 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) + 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.debug('Login status', loginResponse) + } catch (error) { + this.logger.warn(`Something went wrong with the login ${error}`) + } + } + + async docsis(): Promise { + const {data: docsisStatus} = await this.httpClient.get('/api/v1/sta_docsis_status') + return docsisStatus + } + + async logout(): Promise { + 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 + } +} +