Firmware dialects for the TP-Link router API.
A Dialect describes one firmware family's wire protocol as a flat
table of pure functions — one request builder and one parser per protocol
step. Dialects perform no I/O and hold no state: builders return a Request,
parsers take a string or Headers. authenticate and execute own fetch,
sequencing and crypto, and never branch on which dialect they hold.
Built-in dialects
| Dialect | Wire shape | Models |
|---|---|---|
gdprText |
text blocks over /cgi_gdpr |
TL-MR6400, Archer VR900v, TL-MR6500v, Archer MR600 v2 |
gdprJson |
JSON over /cgi_gdpr?9 |
EX220 — also TP-LINK NE200 and probably VX800v, both unconfirmed on hardware |
Dialects are named by protocol shape, never by model, and there is no runtime model registry — a registry is exactly what a third party could not extend without editing this package. The model-to-dialect mapping above is documentation.
Authoring a dialect
Spread an existing dialect and override only what differs. Dialect
has no optional members on purpose, so spreading is what supplies defaults.
Select a dialect for a router that speaks JSON
Select a dialect for a router that speaks JSON
import { assertEquals } from "@std/assert"; import { gdprJson, gdprText } from "./mod.ts"; assertEquals(gdprText.defaultUsername, "admin"); assertEquals(gdprJson.defaultUsername, "user");
Derive a dialect for a model that differs in one place
Derive a dialect for a model that differs in one place
import { assertEquals } from "@std/assert"; import { type Dialect, gdprJson } from "./mod.ts"; const vx800v: Dialect = { ...gdprJson, id: "vx800v", publicKeyRequest: (baseUrl) => new Request(new URL("cgi/getParm", baseUrl), { method: "POST" }), }; assertEquals( vx800v.publicKeyRequest("http://192.168.1.1").url, "http://192.168.1.1/cgi/getParm", ); assertEquals(vx800v.defaultUsername, "user");
Login and busy state reported by the router before authentication.
-
isBusy: boolean
Whether the router is busy serving another management session.
-
isLoggedIn: boolean
Whether a session is already established from another client.
One HTTP round trip's worth of actions.
-
indices: readonly number[]
Indices into the caller's actions array, in the order the response returns them.
-
payload: string
Plaintext payload for this round trip, pre-encryption.
Credentials as supplied by the caller, after the dialect's username default has been applied.
-
password: string
Account password, verbatim.
-
username: string
Account name, defaulted from
Dialect.defaultUsername.
Decoded result of one round trip, positionally aligned with
CommandBatch.indices.
-
error: number | null
Router error code, or
nullwhen the batch reported no error. -
results: readonly (Record<string, string> | Record<string, string>[] | null)[]
One entry per index in the batch;
nullwhere the router returned nothing.
A firmware family's wire protocol, expressed as pure functions and data.
-
busyRequest(baseUrl: string): Request
Builds the request that reports whether a session is already established.
-
commandRequest(): RequestbaseUrl: string,envelope: Envelope,session: SessionContext
Builds a command request for one batch's envelope.
-
decodeCommand(): DecodedBatchtext: string,batch: CommandBatch
Decodes a decrypted command response into results aligned with the batch.
-
defaultUsername: string
Username used when the caller supplies none.
-
encodeCommands(actions: readonly Action[]): readonly CommandBatch[]
Splits actions into round trips and serializes each. Owns the operation vocabulary, stack defaults, and batching.
-
encodeLogin(credentials: Credentials): string
Encodes credentials into the plaintext login payload, pre-encryption.
-
id: string
Stable identifier, carried on the authentication result for diagnostics.
-
infoRequest(baseUrl: string): Request
Builds the request for the login page, whose inline script carries
authTimesand friends. -
loginRequest(): RequestbaseUrl: string,envelope: Envelope
Builds the login request. Owns where the envelope rides — query string, request body, or anywhere else the firmware expects it.
-
parseBusy(text: string): BusyStatus
Extracts login and busy flags from the busy response.
-
parseInfo(html: string): RouterInfo
Scrapes router variables out of the login page.
-
parsePublicKey(text: string): PublicKeyInfo
Extracts RSA parameters from the public key response.
-
parseSessionId(headers: Headers): string | null
Extracts the session id from the login response headers.
-
parseTokenId(html: string): string
Extracts the security token from the authenticated page.
-
publicKeyRequest(baseUrl: string): Request
Builds the request for the RSA public key and sequence base.
-
tokenRequest(): RequestbaseUrl: string,session: Pick<SessionContext, "sessionId" | "authTimes">
Builds the request for the authenticated page carrying the security token.
Encrypted request envelope: AES ciphertext plus the RSA-encrypted signature.
-
data: string
Base64 AES-CBC ciphertext of the plaintext payload.
-
sign: string
Hex RSA-encrypted parameter string:
key&iv&h&sfor login,h&sfor commands.
RSA public key material and the request sequence base.
-
exponent: Uint8Array
RSA public exponent, big-endian.
-
modulus: Uint8Array
RSA modulus, big-endian. Its length fixes the RSA chunk size.
-
sequence: number
Sequence base that every signature's
sparameter is derived from.
Variables scraped from the router's login page.
-
authTimes: number
Login attempt counter, sent back as the
loginErrorShowcookie.
Session material carried on every request made after login.
-
authTimes: number
Login attempt counter, sent back as the
loginErrorShowcookie. -
sessionId: string
JSESSIONIDcookie value returned by the login response. -
tokenId: string
Token scraped from the authenticated landing page.
Action tuple representing a single router command.
Numeric action type value from the ACT constant.
Action type constants for TP-Link router commands.
EU/GDPR firmware speaking a JSON payload format over /cgi_gdpr?9.
EU/GDPR firmware speaking the bespoke text payload format over /cgi_gdpr.
Usage
import * as mod from "dialect/mod.ts";