From 1e49fbe9be1a0dd8297c7c2c39c206f3d356519b Mon Sep 17 00:00:00 2001 From: hensm Date: Tue, 26 Feb 2019 01:30:30 +0000 Subject: [PATCH] Finish app conversion and enforce code style --- app/@types/bplist-creator/index.d.ts | 15 ++ app/@types/bplist-parser/index.d.ts | 23 +++ app/@types/castv2/index.d.ts | 63 +++++---- app/@types/fast-srp-hap/index.d.ts | 55 +++++++ app/package-lock.json | 173 +++++++++++++++++++++++ app/package.json | 7 +- app/src/Media.ts | 40 +++--- app/src/Session.ts | 153 ++++++++++---------- app/src/airplay/{auth.js => auth.ts} | 73 ++++++---- app/src/airplay/{bplist.js => bplist.ts} | 2 +- app/src/main.ts | 38 +++-- app/src/transforms.ts | 16 +-- app/src/types.ts | 4 +- app/tsconfig.json | 1 + app/tslint.json | 24 ++++ ext/package.json | 1 - ext/tsconfig.json | 12 +- package-lock.json | 21 +++ package.json | 6 +- 19 files changed, 541 insertions(+), 186 deletions(-) create mode 100644 app/@types/bplist-creator/index.d.ts create mode 100644 app/@types/bplist-parser/index.d.ts create mode 100644 app/@types/fast-srp-hap/index.d.ts rename app/src/airplay/{auth.js => auth.ts} (73%) rename app/src/airplay/{bplist.js => bplist.ts} (66%) create mode 100644 app/tslint.json diff --git a/app/@types/bplist-creator/index.d.ts b/app/@types/bplist-creator/index.d.ts new file mode 100644 index 0000000..d6710b9 --- /dev/null +++ b/app/@types/bplist-creator/index.d.ts @@ -0,0 +1,15 @@ +/// + +declare module "bplist-creator" { + import Buffer from "buffer"; + + function bplist (dicts: object): Buffer; + export = bplist; + + namespace bplist { + export class Real { + public value: number; + constructor (value: number); + } + } +} diff --git a/app/@types/bplist-parser/index.d.ts b/app/@types/bplist-parser/index.d.ts new file mode 100644 index 0000000..4eadfe4 --- /dev/null +++ b/app/@types/bplist-parser/index.d.ts @@ -0,0 +1,23 @@ +/// + +declare module "bplist-parser" { + import Buffer from "buffer"; + + export var maxObjectSize: number; + export var maxObjectCount: number; + + export class UID { + constructor (id: number); + UID: number; + } + + type ParseFileCallback = ( + err: string + , result?: Buffer) => void; + + export function parseFile ( + fileNameOrBuffer: Buffer | string + , callback: ParseFileCallback): void; + + export function parseBuffer (buffer: Buffer): any; +} diff --git a/app/@types/castv2/index.d.ts b/app/@types/castv2/index.d.ts index 0b2efec..1645eff 100644 --- a/app/@types/castv2/index.d.ts +++ b/app/@types/castv2/index.d.ts @@ -1,16 +1,17 @@ +/// + declare module "castv2" { import { EventEmitter } from "events"; interface ClientConnectOptions { - host: string - , port?: number + host: string; + port?: number; } - interface ClientConnectCallback { - (): void; - } + type CallbackFunction = () => void; - export interface ClientChannel extends EventEmitter { + + export interface Channel extends EventEmitter { bus: Client; sourceId: string; destinationId: string; @@ -21,41 +22,47 @@ declare module "castv2" { close (): void; } - interface ServerListenCallback { - (): void; + export interface DeviceAuthMessage { + parse (data: any): any; + serialize (data: any): any; } export class Client extends EventEmitter { - connect (host: string, callback?: ClientConnectCallback): void; - connect (options: ClientConnectOptions, callback: ClientConnectCallback): void; + public connect ( + options: ClientConnectOptions | string + , callback?: CallbackFunction): void; - close (): void; + public close (): void; - send (sourceId: string - , destinationId: string - , namespace: string - , data: Buffer | string): void; + public send ( + sourceId: string + , destinationId: string + , namespace: string + , data: Buffer | string): void; - createChannel (sourceId: string - , destinationId: string - , namespace: string - , encoding: string): ClientChannel; + public createChannel ( + sourceId: string + , destinationId: string + , namespace: string + , encoding: string): Channel; } - export class Server { + export class Server extends EventEmitter { constructor (options: object); - listen (port: number + public listen ( + port: number , host: string - , callback: ServerListenCallback): void; + , callback?: CallbackFunction): void; - send (clientId: string - , sourceId: string - , destinationId: string - , namespace: string - , data: Buffer | string): void; + public send ( + clientId: string + , sourceId: string + , destinationId: string + , namespace: string + , data: Buffer | string): void; - close (): void; + public close (): void; } } diff --git a/app/@types/fast-srp-hap/index.d.ts b/app/@types/fast-srp-hap/index.d.ts new file mode 100644 index 0000000..134e503 --- /dev/null +++ b/app/@types/fast-srp-hap/index.d.ts @@ -0,0 +1,55 @@ +/// + +declare module "fast-srp-hap" { + import Buffer from "buffer"; + + interface Param { + N_length_bits: number; + N: any; + g: any; + hash: string; + } + + export const params: { [key: number]: Param }; + + type GenKeyCallback = ( + err: string + , buf: Buffer) => void; + + export function genKey ( + bytes: number + , callback: GenKeyCallback): void; + + export function computeVerifier ( + params: object + , salt: Buffer + , I: Buffer + , P: Buffer): Buffer; + + export class Client { + constructor ( + params: object + , salt_buf: Buffer + , identity_buf: Buffer + , password_buf: Buffer + , secret1_buf: Buffer); + + computeA (): Buffer; + setB(B_buf: Buffer): void; + computeM1 (): Buffer; + checkM2 (serverM2_buf: Buffer): void; + computeK (): Buffer; + } + + export class Server { + constructor ( + params: object + , verifier_buf: Buffer + , secret2_buf: Buffer); + + computeB (): Buffer; + setA (A_buf: Buffer): void; + checkM1 (clientM1_buf: Buffer): Buffer; + computeK (): Buffer; + } +} diff --git a/app/package-lock.json b/app/package-lock.json index 4f08573..6454e31 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -46,6 +46,15 @@ "integrity": "sha512-vVjM0SVzgaOUpflq4GYBvCpozes8OgIIS5gVXVka+OfK3hvnkC1i93U8WiY2OtNE4XUWyyy/86Kf6e0IHTQw1Q==", "dev": true }, + "@types/node-fetch": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.1.6.tgz", + "integrity": "sha512-Hv1jgh3pfpUEl2F2mqUd1AfLSk1YbUCeBJFaP36t7esAO617dErqdxWb5cdG2NfJGOofkmBW36fdx0dVewxDRg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "ajv": { "version": "5.5.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", @@ -58,6 +67,12 @@ "json-schema-traverse": "^0.3.0" } }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -67,6 +82,15 @@ "color-convert": "^1.9.0" } }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -166,6 +190,44 @@ "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", "dev": true }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", @@ -336,6 +398,12 @@ "resolved": "https://registry.npmjs.org/bufferview/-/bufferview-1.0.1.tgz", "integrity": "sha1-ev10pF+Tf6QiodM4wIu/3HbNcl0=" }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, "byline": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", @@ -468,6 +536,12 @@ "delayed-stream": "~1.0.0" } }, + "commander": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", + "dev": true + }, "component-emitter": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", @@ -594,6 +668,12 @@ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "dev": true }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, "dir-glob": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz", @@ -1024,6 +1104,15 @@ "har-schema": "^2.0.0" } }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -1274,6 +1363,30 @@ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", "dev": true }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.1.tgz", + "integrity": "sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + } + } + }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", @@ -1837,6 +1950,15 @@ "throttleit": "^1.0.0" } }, + "resolve": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", + "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -2049,6 +2171,12 @@ "extend-shallow": "^3.0.0" } }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, "sshpk": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz", @@ -2124,6 +2252,15 @@ "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==", "dev": true }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -2190,6 +2327,42 @@ "punycode": "^1.4.1" } }, + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "dev": true + }, + "tslint": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.13.0.tgz", + "integrity": "sha512-ECOOQRxXCYnUUePG5h/+Z1Zouobk3KFpIHA9aKBB/nnMxs97S1JJPDGt5J4cGm1y9U9VmVlfboOxA8n1kSNzGw==", + "dev": true, + "requires": { + "babel-code-frame": "^6.22.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^3.2.0", + "glob": "^7.1.1", + "js-yaml": "^3.7.0", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.27.2" + } + }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/app/package.json b/app/package.json index 71f2030..ce084c6 100644 --- a/app/package.json +++ b/app/package.json @@ -8,7 +8,8 @@ "build": "node bin/build.js", "package": "node bin/build.js --package", "install-manifest": "node bin/install-manifest.js", - "remove-manifest": "node bin/install-manifest.js --remove" + "remove-manifest": "node bin/install-manifest.js --remove", + "lint": "tslint -c tslint.json -p ." }, "dependencies": { "bplist-creator": "0.0.7", @@ -24,8 +25,10 @@ "@types/dnssd": "^0.4.1", "@types/mime-types": "^2.1.0", "@types/node": "^11.9.5", + "@types/node-fetch": "^2.1.6", "mustache": "^3.0.1", - "pkg": "^4.3.5" + "pkg": "^4.3.5", + "tslint": "^5.13.0" }, "optionalDependencies": { "rage-edit": "^1.2.0" diff --git a/app/src/Media.ts b/app/src/Media.ts index 63915cb..3627ead 100644 --- a/app/src/Media.ts +++ b/app/src/Media.ts @@ -25,25 +25,24 @@ export interface UpdateMessageData { export default class Media { private sessionId: number; private mediaSessionId: number; - private _id: string; + private referenceId: string; private session: Session; private channel: any; - private _sendMessage: SendMessageCallback; + private sendMessageCallback: SendMessageCallback; - constructor (sessionId: number - , mediaSessionId: number - , _id: string - , parentSession: Session, - _sendMessage: SendMessageCallback) { - - this._id = _id; - - this._sendMessage = _sendMessage; + constructor ( + sessionId: number + , mediaSessionId: number + , referenceId: string + , session: Session + , sendMessageCallback: SendMessageCallback) { this.sessionId = sessionId; this.mediaSessionId = mediaSessionId; + this.referenceId = referenceId; + this.session = session; + this.sendMessageCallback = sendMessageCallback; - this.session = parentSession; this.session.createChannel(MEDIA_NAMESPACE); this.channel = this.session.channelMap.get(MEDIA_NAMESPACE); @@ -54,11 +53,12 @@ export default class Media { const status = data.status[0]; const messageData = { - currentTime: status.currentTime - , _lastCurrentTime: Date.now() / 1000 - , customData: status.customData + _lastCurrentTime: Date.now() / 1000 , _volumeLevel: status.volume.level , _volumeMuted: status.volume.muted + + , currentTime: status.currentTime + , customData: status.customData , playbackRate: status.playbackRate , playerState: status.playerState , repeatMode: status.repeatMode @@ -81,7 +81,7 @@ export default class Media { }); } - messageHandler (message: Message) { + public messageHandler (message: Message) { switch (message.subject) { case "bridge:/media/sendMediaMessage": { let error = false; @@ -97,15 +97,15 @@ export default class Media { }); break; - }; + } } } - sendMessage (subject: string, data: any = {}) { - this._sendMessage({ + private sendMessage (subject: string, data: any = {}) { + this.sendMessageCallback({ subject , data - , _id: this._id + , _id: this.referenceId }); } } diff --git a/app/src/Session.ts b/app/src/Session.ts index 3358841..5aa3534 100644 --- a/app/src/Session.ts +++ b/app/src/Session.ts @@ -1,6 +1,8 @@ "use strict"; -import { Client, ClientChannel } from "castv2"; +import uuid from "uuid"; + +import { Channel, Client } from "castv2"; import { Message , SendMessageCallback } from "./types"; @@ -11,40 +13,39 @@ const NS_HEARTBEAT = "urn:x-cast:com.google.cast.tp.heartbeat"; const NS_RECEIVER = "urn:x-cast:com.google.cast.receiver"; export default class Session { - private _sendMessage: SendMessageCallback; + public channelMap = new Map(); + + private sendMessageCallback: SendMessageCallback; private sessionId: number; - private _id: string; + private referenceId: string; private client: Client; - private clientConnection: ClientChannel; - private clientHeartbeat: ClientChannel; - private clientReceiver: ClientChannel; + private clientConnection: Channel; + private clientHeartbeat: Channel; + private clientReceiver: Channel; private clientHeartbeatIntervalId: NodeJS.Timer; private isSessionCreated = false; private clientId: string; private transportId: string; - private transportConnection: ClientChannel; + private transportConnection: Channel; private app: any; + constructor ( + host: string + , port: number + , appId: string + , sessionId: number + , sendMessageCallback: SendMessageCallback) { - public channelMap = new Map(); - - - constructor (host: string - , port: number - , appId: string - , sessionId: number - , _sendMessage: SendMessageCallback) { - - this._sendMessage = _sendMessage; + this.sendMessageCallback = sendMessageCallback; this.sessionId = sessionId; this.client = new Client(); this.client.connect({ host, port }, () => { - let transportHeartbeat: ClientChannel; + let transportHeartbeat: Channel; const sourceId = "sender-0"; const destinationId = "receiver-0"; @@ -63,6 +64,7 @@ export default class Session { if (transportHeartbeat) { transportHeartbeat.send({ type: "PING" }); } + this.clientHeartbeat.send({ type: "PING" }); }, 5000); @@ -73,56 +75,58 @@ export default class Session { }); this.clientReceiver.on("message", (message: any) => { - switch (message.type) { - case "RECEIVER_STATUS": { - this.sendMessage("shim:/session/updateStatus", message.status); + if (message.type === "RECEIVER_STATUS") { + this.sendMessage("shim:/session/updateStatus" + , message.status); - if (!message.status.applications) return; + if (!message.status.applications) { + return; + } - const receiverApp = message.status.applications[0]; - const receiverAppId = receiverApp.appId; + const receiverApp = message.status.applications[0]; + const receiverAppId = receiverApp.appId; - this.app = receiverApp; + this.app = receiverApp; - if (receiverAppId !== appId) { - // Close session - this.sendMessage("shim:/session/stopped"); - this.client.close(); - clearInterval(this.clientHeartbeatIntervalId); - return; - } + if (receiverAppId !== appId) { + // Close session + this.sendMessage("shim:/session/stopped"); + this.client.close(); + clearInterval(this.clientHeartbeatIntervalId); + return; + } - if (!this.isSessionCreated) { - this.isSessionCreated = true; + if (!this.isSessionCreated) { + this.isSessionCreated = true; - this.transportId = this.app.transportId; - this.clientId = `client-${Math.floor(Math.random() * 10e5)}`; + this.transportId = this.app.transportId; + this.clientId = + `client-${Math.floor(Math.random() * 10e5)}`; - this.transportConnection = this.client.createChannel( - this.clientId, this.transportId, NS_CONNECTION, "JSON"); - transportHeartbeat = this.client.createChannel( - this.clientId, this.transportId, NS_HEARTBEAT, "JSON"); + this.transportConnection = this.client.createChannel( + this.clientId, this.transportId + , NS_CONNECTION, "JSON"); + transportHeartbeat = this.client.createChannel( + this.clientId, this.transportId + , NS_HEARTBEAT, "JSON"); - this.transportConnection.send({ type: "CONNECT" }); + this.transportConnection.send({ type: "CONNECT" }); - this.sessionId = this.app.sessionId; + this.sessionId = this.app.sessionId; - this.sendMessage("shim:/session/connected", { - sessionId: this.app.sessionId - , namespaces: this.app.namespaces - , displayName: this.app.displayName - , statusText: this.app.displayName - }); - } - - break; - }; + this.sendMessage("shim:/session/connected", { + sessionId: this.app.sessionId + , namespaces: this.app.namespaces + , displayName: this.app.displayName + , statusText: this.app.displayName + }); + } } }); }); } - messageHandler (message: Message) { + public messageHandler (message: Message) { switch (message.subject) { case "bridge:/session/close": this.close(); @@ -136,7 +140,7 @@ export default class Session { this._impl_sendMessage( message.data.namespace , message.data.message - , message.data.messageId) + , message.data.messageId); break; case "bridge:/session/impl_setReceiverMuted": @@ -157,15 +161,7 @@ export default class Session { } } - sendMessage (subject: string, data: any = {}) { - this._sendMessage({ - subject - , data - , _id: this._id - }); - } - - createChannel (namespace: string) { + public createChannel (namespace: string) { if (!this.channelMap.has(namespace)) { this.channelMap.set(namespace , this.client.createChannel( @@ -173,25 +169,36 @@ export default class Session { } } - close () { + public close () { this.clientConnection.send({ type: "CLOSE" }); if (this.transportConnection) { this.transportConnection.send({ type: "CLOSE" }); } } + private sendMessage (subject: string, data: any = {}) { + this.sendMessageCallback({ + subject + , data + , _id: this.referenceId + }); + } - _impl_addMessageListener (namespace: string) { + private _impl_addMessageListener (namespace: string) { this.createChannel(namespace); this.channelMap.get(namespace).on("message", (data: any) => { this.sendMessage("shim:/session/impl_addMessageListener", { - namespace: namespace + namespace , data: JSON.stringify(data) }); - }) + }); } - _impl_sendMessage (namespace: string, message: object, messageId: string) { + private _impl_sendMessage ( + namespace: string + , message: object + , messageId: string) { + let error = false; try { @@ -207,7 +214,8 @@ export default class Session { }); } - _impl_setReceiverMuted (muted: boolean, volumeId: string) { + private _impl_setReceiverMuted (muted: boolean, volumeId: string) { + let error = false; try { @@ -226,7 +234,8 @@ export default class Session { }); } - _impl_setReceiverVolumeLevel (newLevel: number, volumeId: string) { + private _impl_setReceiverVolumeLevel (newLevel: number, volumeId: string) { + let error = false; try { @@ -234,7 +243,7 @@ export default class Session { type: "SET_VOLUME" , volume: { level: newLevel } , requestId: 0 - }) + }); } catch (err) { error = true; } @@ -245,7 +254,7 @@ export default class Session { }); } - _impl_stop (stopId: string) { + private _impl_stop (stopId: string) { let error = false; try { diff --git a/app/src/airplay/auth.js b/app/src/airplay/auth.ts similarity index 73% rename from app/src/airplay/auth.js rename to app/src/airplay/auth.ts index 4556882..c8e196a 100644 --- a/app/src/airplay/auth.js +++ b/app/src/airplay/auth.ts @@ -8,10 +8,10 @@ * - https://github.com/postlund/pyatv/blob/master/docs/airplay.rst */ -import nacl from "tweetnacl"; -import srp6a from "fast-srp-hap"; import crypto from "crypto"; -import fetch from "node-fetch"; +import srp6a from "fast-srp-hap"; +import fetch, { Headers } from "node-fetch"; +import nacl from "tweetnacl"; import bplist from "./bplist"; @@ -22,7 +22,11 @@ const MIMETYPE_BPLIST = "application/x-apple-binary-plist"; * Client ID and keypair */ export class AirPlayAuthCredentials { - constructor (clientId, clientSk) { + public clientId: string; + public clientSk: Uint8Array; + public clientPk: Uint8Array; + + constructor (clientId: string, clientSk: Uint8Array) { if (clientId && clientSk) { this.clientId = clientId; this.clientSk = clientSk; @@ -40,11 +44,13 @@ export class AirPlayAuthCredentials { } export class AirPlayAuth { - constructor (address, credentials) { + private address: string; + private credentials: AirPlayAuthCredentials; + private baseUrl: URL; + + constructor (address: string, credentials: AirPlayAuthCredentials) { this.address = address; - this.clientId = credentials.clientId; - this.clientSk = credentials.clientSk; - this.clientPk = credentials.clientPk; + this.credentials = credentials; this.baseUrl = new URL(`http://${this.address}:${AIRPLAY_PORT}`); } @@ -52,7 +58,7 @@ export class AirPlayAuth { /** * Begins pairing process. */ - async beginPairing () { + public async beginPairing () { return this.sendPostRequest("/pair-pin-start"); } @@ -61,7 +67,7 @@ export class AirPlayAuth { * beginPairing(). Coordinates the three pairing stages and * manages request responses. */ - async finishPairing (pin) { + public async finishPairing (pin: string) { // Stage 1 response const { pk: serverPk , salt: serverSalt } = await this.pairSetupPin1(); @@ -72,11 +78,11 @@ export class AirPlayAuth { // Create SRP client const srpClient = new srp6a.Client( - srpParams // Params - , serverSalt // Receiver salt - , Buffer.from(this.clientId) // Username - , Buffer.from(pin) // Password (receiver pin) - , Buffer.from(this.clientSk)); // Client secret key + srpParams // Params + , serverSalt // Receiver salt + , Buffer.from(this.credentials.clientId) // Username + , Buffer.from(pin) // Password (receiver pin) + , Buffer.from(this.credentials.clientSk)); // Client secret key // Add receiver's public key srpClient.setB(serverPk); @@ -87,7 +93,7 @@ export class AirPlayAuth { , srpClient.computeM1()); // SRP proof // Stage 3 response - console.log(await this.pairSetupPin3(srpClient.computeK())); + await this.pairSetupPin3(srpClient.computeK()); } /** @@ -96,12 +102,12 @@ export class AirPlayAuth { * Triggering the receiver passcode display and receiving * its public key / salt. */ - async pairSetupPin1 () { + public async pairSetupPin1 (): Promise { const [ response ] = await this.sendPostRequestBplist( "/pair-setup-pin" , { method: "pin" - , user: this.clientId + , user: this.credentials.clientId }); return response; @@ -114,7 +120,10 @@ export class AirPlayAuth { * public keys, sending them to the receiver and receiving its * proof. */ - async pairSetupPin2 (pk, proof) { + public async pairSetupPin2 ( + pk: Buffer + , proof: Buffer): Promise { + const [ response ] = await this.sendPostRequestBplist( "/pair-setup-pin" , { pk, proof }); @@ -129,7 +138,9 @@ export class AirPlayAuth { * secret hash and sending it to the receiver. Receiver then * responds confirming the pairing is complete. */ - async pairSetupPin3 (sharedSecretHash) { + public async pairSetupPin3 ( + sharedSecretHash: crypto.BinaryLike): Promise { + // Create AES key const aesKey = crypto.createHash("sha512") .update("Pair-Setup-AES-Key") @@ -150,7 +161,7 @@ export class AirPlayAuth { const cipher = crypto.createCipheriv("aes-128-gcm", aesKey, aesIv); // Encode client public key - const epk = cipher.update(this.clientPk); + const epk = cipher.update(this.credentials.clientPk); cipher.final(); const authTag = cipher.getAuthTag(); @@ -166,7 +177,11 @@ export class AirPlayAuth { * Sends a POST request to receiver and returns the * response. */ - async sendPostRequest (path, contentType, data) { + public async sendPostRequest ( + path: string + , contentType?: string + , data?: Buffer | string): Promise { + // Create URL from base receiver URL and path const requestUrl = new URL(path, this.baseUrl); @@ -189,24 +204,26 @@ export class AirPlayAuth { throw new Error(`AirPlay request error: ${response.status}`); } - return await response.arrayBuffer(); + return await response.buffer(); } /** * Encodes binary plist data, sends a POST request to * receiver, then decodes and returns the response. */ - async sendPostRequestBplist (path, data) { + public async sendPostRequestBplist ( + path: string + , data?: object): Promise { + // Convert data to compatible type const requestBody = data ? bplist.create(data) - : null; + : undefined; - const responseArrayBuffer = await this.sendPostRequest( + const response = await this.sendPostRequest( path, MIMETYPE_BPLIST, requestBody); // Convert response data to Buffer for bplist-parser - return bplist.parse.parseBuffer( - Buffer.from(responseArrayBuffer)); + return bplist.parse.parseBuffer(response); } } diff --git a/app/src/airplay/bplist.js b/app/src/airplay/bplist.ts similarity index 66% rename from app/src/airplay/bplist.js rename to app/src/airplay/bplist.ts index 1feb3a4..a0e1744 100644 --- a/app/src/airplay/bplist.js +++ b/app/src/airplay/bplist.ts @@ -1,4 +1,4 @@ import create from "bplist-creator"; -import parse from "bplist-parser"; +import parse from "bplist-parser"; export default { create, parse }; diff --git a/app/src/main.ts b/app/src/main.ts index 296e079..d10f0e8 100755 --- a/app/src/main.ts +++ b/app/src/main.ts @@ -1,13 +1,14 @@ import dnssd from "dnssd"; -import http from "http"; +import events from "events"; import fs from "fs"; -import path from "path"; +import http from "http"; import mime from "mime-types"; +import path from "path"; -import * as transforms from "./transforms"; import Media from "./Media"; import Session from "./Session"; +import * as transforms from "./transforms"; import { Message } from "./types"; @@ -15,18 +16,21 @@ import { __applicationName , __applicationVersion } from "../package.json"; +// Increase listener limit +events.EventEmitter.defaultMaxListeners = 50; + + const browser = new dnssd.Browser(dnssd.tcp("googlecast")); // Local media server let httpServer: http.Server; process.on("SIGTERM", () => { - if (httpServer) httpServer.close(); + if (httpServer) { + httpServer.close(); + } }); -// Increase listener limit -require("events").EventEmitter.defaultMaxListeners = 50; - // stdin -> stdout process.stdin .pipe(transforms.decode) @@ -40,7 +44,9 @@ process.stdin function sendMessage (message: object) { try { transforms.encode.write(message); - } catch (err) {} + } catch (err) { + console.error("Failed to encode message"); + } } @@ -102,12 +108,12 @@ async function handleMessage (message: Message) { case "bridge:/getInfo": { const extensionVersion = message.data; return __applicationVersion; - }; + } case "bridge:/startDiscovery": { browser.start(); break; - }; + } case "bridge:/startHttpServer": { const { filePath, port } = message.data; @@ -144,7 +150,7 @@ async function handleMessage (message: Message) { , "Content-Type": contentType }); - fs.createReadStream(filePath).pipe(res) + fs.createReadStream(filePath).pipe(res); } }); @@ -155,12 +161,14 @@ async function handleMessage (message: Message) { }); break; - }; + } case "bridge:/stopHttpServer": { - if (httpServer) httpServer.close(); + if (httpServer) { + httpServer.close(); + } break; - }; + } } } @@ -180,7 +188,7 @@ browser.on("serviceUp", (service: dnssd.Service) => { browser.on("serviceDown", (service: dnssd.Service) => { transforms.encode.write({ - subject:"shim:/serviceDown" + subject: "shim:/serviceDown" , data: { id: service.txt.id } diff --git a/app/src/transforms.ts b/app/src/transforms.ts index 9828ef2..d6f4720 100755 --- a/app/src/transforms.ts +++ b/app/src/transforms.ts @@ -4,9 +4,7 @@ import { Transform } from "stream"; import { Message } from "./types"; -interface ResponseHandlerFunction { - (message: Message): Promise -} +type ResponseHandlerFunction = (message: Message) => Promise; /** * Takes a handler function that implements the transform @@ -18,9 +16,9 @@ export const response = (handler: ResponseHandlerFunction) => new Transform({ , transform (chunk: Message, encoding, callback) { Promise.resolve(handler(chunk)) - .then(response => { - if (response) { - callback(null, response); + .then(res => { + if (res) { + callback(null, res); } else { callback(null); } @@ -94,13 +92,13 @@ export const encode = new Transform({ writableObjectMode: true , transform (chunk, encoding, callback) { - const message_length = Buffer.alloc(4); + const messageLength = Buffer.alloc(4); const message = Buffer.from(JSON.stringify(chunk)); // Write message length - message_length.writeUInt32LE(message.length, 0); + messageLength.writeUInt32LE(message.length, 0); // Output joined message length and content - callback(null, Buffer.concat([message_length, message])); + callback(null, Buffer.concat([messageLength, message])); } }); diff --git a/app/src/types.ts b/app/src/types.ts index 527a3ab..4759a36 100644 --- a/app/src/types.ts +++ b/app/src/types.ts @@ -6,6 +6,4 @@ export interface Message { _id?: string; } -export interface SendMessageCallback { - (message: Message): void -} +export type SendMessageCallback = (message: Message) => void; diff --git a/app/tsconfig.json b/app/tsconfig.json index a710eb6..82612d0 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -6,6 +6,7 @@ , "esModuleInterop": true , "resolveJsonModule": true , "removeComments": true + , "downlevelIteration": true } , "include": [ "./src/**/*" diff --git a/app/tslint.json b/app/tslint.json new file mode 100644 index 0000000..cd2a8ef --- /dev/null +++ b/app/tslint.json @@ -0,0 +1,24 @@ +{ + "defaultSeverity": "error" + , "extends": [ + "tslint:recommended" + ] + , "jsRules": {} + , "rules": { + "no-consecutive-blank-lines": false + , "arrow-parens": false + , "interface-name": false + , "max-classes-per-file": false + , "max-line-length": [ true, { + "limit": 80 + , "ignore-pattern": "//" + }] + , "no-console": [ true, "log" ] + , "object-literal-sort-keys": false + , "radix": false + , "semicolon": [ true, "always" ] + , "space-before-function-paren": [ true, "always" ] + , "trailing-comma": false + } + , "rulesDirectory": [] +} diff --git a/ext/package.json b/ext/package.json index 6e0ee36..1ba58d2 100644 --- a/ext/package.json +++ b/ext/package.json @@ -17,7 +17,6 @@ "preact": "^8.4.2", "preact-compat": "^3.18.4", "ts-loader": "^5.3.3", - "uuid": "^3.3.2", "web-ext": "^2.9.1", "webpack": "^4.27.0", "webpack-cli": "^3.1.2" diff --git a/ext/tsconfig.json b/ext/tsconfig.json index 0ab03d5..e00e3c2 100644 --- a/ext/tsconfig.json +++ b/ext/tsconfig.json @@ -1,14 +1,14 @@ { "compilerOptions": { "allowJs": true - , "sourceMap": true - , "target": "es6" - , "noImplicitAny": true , "esModuleInterop": true - , "moduleResolution": "node" - , "resolveJsonModule": true - , "removeComments": true , "jsx": "react" , "lib": [ "esnext", "dom" ] + , "moduleResolution": "node" + , "noImplicitAny": true + , "removeComments": true + , "resolveJsonModule": true + , "sourceMap": true + , "target": "es6" } } diff --git a/package-lock.json b/package-lock.json index 6bf9b20..8a38abf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2,6 +2,21 @@ "requires": true, "lockfileVersion": 1, "dependencies": { + "@types/node": { + "version": "11.9.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.9.5.tgz", + "integrity": "sha512-vVjM0SVzgaOUpflq4GYBvCpozes8OgIIS5gVXVka+OfK3hvnkC1i93U8WiY2OtNE4XUWyyy/86Kf6e0IHTQw1Q==", + "dev": true + }, + "@types/uuid": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.4.tgz", + "integrity": "sha512-tPIgT0GUmdJQNSHxp0X2jnpQfBSTfGxUMc/2CXBU2mnyTFVYVa2ojpoQ74w0U2yn2vw3jnC640+77lkFFpdVDw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", @@ -468,6 +483,12 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "dev": true + }, "wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", diff --git a/package.json b/package.json index 251c4cc..55f0c6f 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,12 @@ "package:ext": "npm run package --prefix ./ext", "test": "node test/driver.js", "install-manifest": "npm run install-manifest --prefix ./app", - "remove-manifest": "npm run remove-manifest --prefix ./app" + "remove-manifest": "npm run remove-manifest --prefix ./app", + "lint": "npm run lint:app", + "lint:app": "npm run lint --prefix ./app" }, "devDependencies": { + "@types/uuid": "^3.4.4", "fs-extra": "^7.0.1", "glob": "^7.1.3", "jasmine-console-reporter": "^3.1.0", @@ -21,6 +24,7 @@ "selenium-webdriver": "^4.0.0-alpha.1", "semver": "^5.6.0", "typescript": "^3.3.3333", + "uuid": "^3.3.2", "ws": "^6.1.2" } }