source/index.js
import cheerio from 'cheerio'
import request from 'superagent'
import url from 'url'
// internal
import { FritzBoxError, fixPassword, onify, parseLogEntry } from './utilities'
import { md5 } from './crypto'
/**
* @description main class
*/
export default class FritzBoxAPI {
constructor({
allowSelfSignedCertificate = false,
host = 'fritz.box',
password,
secure = false,
username,
}) {
Object.assign(this, {
host,
password,
secure,
username,
})
// @TODO: superagent doesn't support selectively allowing self signed certificates. Migrate to another
// request library. See: https://github.com/visionmedia/superagent/issues?utf8=%E2%9C%93&q=certificate
if (allowSelfSignedCertificate) process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0
}
/**
* @description FRITZ!Box wants this format (md5 encoding is ucs-2):
* response = challenge + '-' + md5.hex(challenge + '-' + password)
* @returns {String} sessionId
*/
async getSession() {
const index = await request.get(this.api('/'))
// get challenge
const matches = index.text.match(/"challenge":\s*"(.+?)",/)
const challenge = matches ? matches[1] : null
if (!challenge) throw new Error('Unable to decode challenge')
// solve challenge
const response = `${challenge}-${md5(
`${challenge}-${fixPassword(this.password)}`
)}`
// attempt sign-in
const signIn = await request
.post(this.api('/'))
.type('form')
.send({
response,
username: this.username,
})
// get sessionID
const start = signIn.text.indexOf('?sid=')
const stop = signIn.text.indexOf('&', start)
return (this.sessionID = signIn.text.substring(start + 5, stop))
}
/**
* @description retrieves guest WLAN settings
* @returns {Object} WLAN settings
*/
async getGuestWLAN() {
const response = await request
.post(this.api('/data.lua'))
.type('form')
.send({
sid: this.sessionID,
page: 'wGuest',
})
const $ = cheerio.load(response.text)
return {
ssid: $('#uiViewGuestSsid').val(),
key: $('#uiViewWpaKey').val(),
active: $('#uiViewActivateGuestAccess').is(':checked'),
limited: $('#uiGroupAccess').is(':checked'),
terms: $('#uiUntrusted').is(':checked'),
allowCommunication: $('#uiUiUserIsolation').is(':checked'),
autoDisable: $('#uiViewDownTimeActiv').is(':checked'),
waitForLastGuest: $('#uiViewDisconnectGuestAccess').is(':checked'),
deactivateAfter: $('#uiViewDownTime').val(),
security: $('#uiSecMode').val(),
}
}
/**
* @description Retrieves connected clients
* @returns {Object} connected clients
*/
async getConnectedClients() {
const response = await request
.post(this.api('/data.lua'))
.type('form')
.send({
sid: this.sessionID,
page: 'homeNet',
})
const $ = cheerio.load(response.text)
const devices = $('.dev_lan')
.map(function() {
const deviceLink = url.parse(
$('.details > .textlink', this).attr('href'),
true
)
return {
id: deviceLink.query.dev,
name: $('.name', this).attr('title'),
}
})
.get()
return devices
}
/**
* @description Gathers more information about a specific device.
* @param {String} id device Id
* @returns {Object} device details
*/
async getDeviceDetails(id) {
const response = await request
.post(this.api('/data.lua'))
.type('form')
.send({
sid: this.sessionID,
dev: id,
oldpage: '/net/edit_device.lua',
})
const $ = cheerio.load(response.text)
return {
id,
name: $('#uiViewDeviceName').val(),
ip: $('#uiViewDeviceIP').val(),
mac: $('#uiDetailsMacContent')
.html()
.substring(0, 17),
}
}
/**
* @description Collects FRITZ!Box logs.
* @param {String} [filter='all'] all, system, internet, phone, wlan, usb
* @returns {Array} log entries
*/
async getLog(filter = 'all') {
const filterId = {
all: 0,
system: 1,
internet: 2,
phone: 3,
wlan: 4,
usb: 5,
}[filter]
const response = await request
.post(this.api('/data.lua'))
.type('form')
.send({
sid: this.sessionID,
xhr: 1,
xhrId: 'all',
page: 'log',
filter: filterId,
})
// wrong content-type, manually parse JSON
try {
const { data } = JSON.parse(response.text)
return data.log.map(parseLogEntry(response.headers.date))
} catch (error) {
throw new FritzBoxError('could not parse logs', error)
}
}
/**
* @description saves guest WLAN settings
*/
async setGuestWlan({
active,
allowCommunication,
autoDisable,
deactivateAfter,
key,
limited,
security,
ssid,
terms,
waitForLastGuest,
}) {
const template = {
sid: this.sessionID,
xhr: 1,
lang: 'de',
no_sidrenew: '',
autoupdate: 'on',
apply: '',
oldpage: '/wlan/guest_access.lua',
}
const response = await request
.post(this.api('/data.lua'))
.type('form')
.send(template)
.send({
activate_guest_access: onify(active),
disconnect_guest_access: onify(waitForLastGuest),
down_time_activ: onify(autoDisable),
down_time_value: deactivateAfter,
guest_ssid: ssid,
sec_mode: security,
wpa_key: key,
})
// deactivate guest WLAN
if (!active)
await request
.post(this.api('/data.lua'))
.type('form')
.send(template)
}
/**
* @description retrieves a network overviews
* @returns {Object} overview
*/
async overview() {
const response = await request
.post(this.api('/data.lua'))
.type('form')
.send({
sid: this.sessionID,
xhr: 1,
lang: 'de',
page: 'overview',
type: 'all',
no_sidrenew: '',
})
return JSON.parse(response.text)
}
/**
* @description helper method
* @param {String} endpoint API endpoint
* @returns {String} API endpoint URL
*/
api(endpoint) {
return `http${this.secure ? 's' : ''}://${this.host}${endpoint}`
}
}