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"
}
}