tp-link-legacy-api
v0.4.0
Published
Client Node.js pour l'interface web des routeurs TP-Link « legacy » (TL-WR841N v13/v14 et proches, firmwares /cgi_gdpr)
Maintainers
Readme
tp-link-legacy-api
Read this in French.
Node.js client (ESM, zero dependency) for the web interface of legacy TP-Link
routers — the firmwares exposing /cgi_gdpr, including the TL-WR841N
v13/v14.
It speaks the router's protocol directly: no browser, no execution of the firmware's own scripts.
Install
npm install tp-link-legacy-apiNode ≥ 18.
Library
import { TpLinkRouter } from "tp-link-legacy-api";
const router = new TpLinkRouter({ host: "192.168.0.1", password: "…" });
await router.getInfo(); // model, firmware, uptime, mode
await router.getLan(); // IP, netmask, MAC, DHCP state
await router.getWan(); // internet connection, public IP, DNS, link
await router.getWireless(); // radios: SSID, channel, security, enabled
await router.getClients(); // DHCP leases + Wi-Fi stations, deduplicated
await router.getStatus(); // everything at once
await router.setWirelessEnabled(false, { band: "2.4GHz" });
await router.setSsid("MyNetwork", { band: "2.4GHz" });
await router.reboot();
await router.disconnect(); // frees the administrator slotgetStatus() isolates each section: a section the firmware refuses returns its
error under status.errors without failing the rest.
For anything the high-level API does not cover, the data model stays reachable:
await router.raw.getList("LAN_WLAN_ASSOC_DEV");
await router.raw.get("IGD_DEV_INFO", { attrs: ["upTime"] });
await router.raw.set("LAN_WLAN", { channel: 6 }, { stack: "1,1,0,0,0,0" });Command line
tp-link-api status --host 192.168.0.1 --password '…'
tp-link-api clients --host 192.168.0.1 --password '…'
tp-link-api wifi --host 192.168.0.1 --password '…' --off --band 2.4GHz
tp-link-api get LAN_WLAN --list --host 192.168.0.1 --password '…'REST server
Meant as a source for Home Assistant.
tp-link-api serve --config routers.json --port 8787 --token MY_TOKEN[
{ "name": "living-room", "host": "192.168.11.1", "password": "…" },
{ "name": "bedroom", "host": "192.168.12.1", "password": "…" }
]| Route | Effect |
|---|---|
| GET /health | service state |
| GET /routers | router list (?status=1 to read them all) |
| GET /routers/:name/status | full snapshot |
| GET /routers/:name/{info,lan,wan,wireless,clients,leases,ports} | one section |
| POST /routers/:name/wireless/:band/enable | { "enabled": false } |
| POST /routers/:name/wireless/:band/ssid | { "ssid": "…" } |
| POST /routers/:name/reboot | reboot |
Environment variables work too: TPLINK_ROUTERS, TPLINK_HOST,
TPLINK_PASSWORD, TPLINK_PORT, TPLINK_TOKEN.
Wi-Fi passphrases are never returned by default, even though the firmware
hands them over in clear; --secrets or ?secrets=1 is required.
⚠️ The client must be on the router's LAN
The firmware enforces a "GDPR" restriction: objects holding personal data —
Wi-Fi passphrase, MAC addresses, WAN credentials — are only readable by a
local client. From another subnet the router answers HTTP 500 on those
objects and exposes only model, firmware and mode.
Check it with:
tp-link-api get /cgi/info --host 192.168.0.1 --password '…'
# clientLocal=1 → everything is readable
# clientLocal=0 → only non-personal data isThis comes from the router, not from this library: the firmware's own JavaScript
receives the same HTTP 500 in that situation. Run the API from a machine on
that LAN — the Home Assistant host, typically.
The protocol, briefly
Reverse-engineered from js/lib.js and js/tpEncrypt.js served by the router.
POST /cgi?8with[/cgi/getParm#…]0,0→ 512-bit RSA public key (nn,ee) and session counter (seq).- The client draws an AES-128-CBC key and IV (16 digits each).
- Every request is
POST /cgi_gdpr:sign=<RSA hex>\r\ndata=<AES base64>\r\ndataencrypts<types>\r\n[<oid>#<stack>#<pStack>]<index>,<n>\r\n<attributes>,signencryptskey=…&iv=…&h=md5(user+password)&s=<seq+len(data)>. - The response is AES base64, decrypted with the same key.
Two traps, for anyone reimplementing this:
- The
Refererheader is mandatory: without it the router answers403on every resource, including its own.jsfiles. - The signature must replay
key=…&iv=…on every request. The firmware also offers a short form (h=…&s=…) which assumes the router still holds the session key in memory; once that context is lost it produces a500and voids the cookie. The full form makes the session self-contained.
The embedded httpd also earns its own HTTP client (src/core/http.js): it
announces Transfer-Encoding: chunked on error pages but sends the body raw,
which breaks both fetch() and node:http, even with insecureHTTPParser.
Home Assistant
A Home Assistant integration built on a Python port of this client lives at GollumDom/tp-link-legacy-integration.
Tests
npm testThe tests cover the protocol (RSA, serialization, response parsing, encryption) and need no router.
License
MIT


