mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
moving smapi soap related classes into smapi.ts
This commit is contained in:
109
src/server.ts
109
src/server.ts
@@ -1,26 +1,17 @@
|
|||||||
import express, { Express } from "express";
|
import express, { Express } from "express";
|
||||||
import * as Eta from "eta";
|
import * as Eta from "eta";
|
||||||
import { listen } from "soap";
|
|
||||||
import { readFileSync } from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import morgan from "morgan";
|
import morgan from "morgan";
|
||||||
import crypto from "crypto";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Sonos,
|
Sonos,
|
||||||
Service,
|
Service,
|
||||||
SOAP_PATH,
|
|
||||||
STRINGS_PATH,
|
|
||||||
PRESENTATION_MAP_PATH,
|
|
||||||
} from "./sonos";
|
} from "./sonos";
|
||||||
|
import { SOAP_PATH, STRINGS_ROUTE, PRESENTATION_MAP_ROUTE, LOGIN_ROUTE } from './smapi';
|
||||||
import { LinkCodes, InMemoryLinkCodes } from "./link_codes";
|
import { LinkCodes, InMemoryLinkCodes } from "./link_codes";
|
||||||
import { MusicService, isSuccess } from "./music_service";
|
import { MusicService, isSuccess } from "./music_service";
|
||||||
import logger from "./logger";
|
// import logger from "./logger";
|
||||||
|
import bindSmapiSoapServiceToExpress from "./smapi";
|
||||||
|
|
||||||
const WSDL_FILE = path.resolve(
|
|
||||||
__dirname,
|
|
||||||
"Sonoswsdl-1.19.4-20190411.142401-3.wsdl"
|
|
||||||
);
|
|
||||||
|
|
||||||
function server(
|
function server(
|
||||||
sonos: Sonos,
|
sonos: Sonos,
|
||||||
@@ -70,14 +61,15 @@ function server(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/login", (req, res) => {
|
app.get(LOGIN_ROUTE, (req, res) => {
|
||||||
res.render("login", {
|
res.render("login", {
|
||||||
bonobService,
|
bonobService,
|
||||||
linkCode: req.query.linkCode,
|
linkCode: req.query.linkCode,
|
||||||
|
loginRoute: LOGIN_ROUTE
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/login", (req, res) => {
|
app.post(LOGIN_ROUTE, (req, res) => {
|
||||||
const { username, password, linkCode } = req.body;
|
const { username, password, linkCode } = req.body;
|
||||||
const authResult = musicService.login({
|
const authResult = musicService.login({
|
||||||
username,
|
username,
|
||||||
@@ -101,7 +93,7 @@ function server(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get(STRINGS_PATH, (_, res) => {
|
app.get(STRINGS_ROUTE, (_, res) => {
|
||||||
res.type("application/xml").send(`<?xml version="1.0" encoding="utf-8" ?>
|
res.type("application/xml").send(`<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<stringtables xmlns="http://sonos.com/sonosapi">
|
<stringtables xmlns="http://sonos.com/sonosapi">
|
||||||
<stringtable rev="1" xml:lang="en-US">
|
<stringtable rev="1" xml:lang="en-US">
|
||||||
@@ -111,94 +103,11 @@ function server(
|
|||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get(PRESENTATION_MAP_PATH, (_, res) => {
|
app.get(PRESENTATION_MAP_ROUTE, (_, res) => {
|
||||||
res.send("");
|
res.send("");
|
||||||
});
|
});
|
||||||
|
|
||||||
const sonosService = {
|
bindSmapiSoapServiceToExpress(app, SOAP_PATH, webAddress, linkCodes);
|
||||||
Sonos: {
|
|
||||||
SonosSoap: {
|
|
||||||
getAppLink: () => {
|
|
||||||
const linkCode = linkCodes.mint();
|
|
||||||
return {
|
|
||||||
getAppLinkResult: {
|
|
||||||
authorizeAccount: {
|
|
||||||
appUrlStringId: "AppLinkMessage",
|
|
||||||
deviceLink: {
|
|
||||||
regUrl: `${webAddress}/login?linkCode=${linkCode}`,
|
|
||||||
linkCode: linkCode,
|
|
||||||
showLinkCode: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getDeviceAuthToken: ({ linkCode }: { linkCode: string }) => {
|
|
||||||
const association = linkCodes.associationFor(linkCode);
|
|
||||||
if (association) {
|
|
||||||
return {
|
|
||||||
getDeviceAuthTokenResult: {
|
|
||||||
authToken: association.authToken.value,
|
|
||||||
privateKey: association.authToken.version,
|
|
||||||
userInfo: {
|
|
||||||
nickname: association.nickname,
|
|
||||||
userIdHashCode: crypto
|
|
||||||
.createHash("sha256")
|
|
||||||
.update(association.userId)
|
|
||||||
.digest("hex"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
throw {
|
|
||||||
Fault: {
|
|
||||||
faultcode: "Client.NOT_LINKED_RETRY",
|
|
||||||
faultstring: "Link Code not found retry...",
|
|
||||||
detail: {
|
|
||||||
ExceptionInfo: "NOT_LINKED_RETRY",
|
|
||||||
SonosError: "5",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getSessionId: ({
|
|
||||||
username,
|
|
||||||
}: {
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
}) => {
|
|
||||||
return Promise.resolve({
|
|
||||||
username,
|
|
||||||
sessionId: "123",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const x = listen(
|
|
||||||
app,
|
|
||||||
SOAP_PATH,
|
|
||||||
sonosService,
|
|
||||||
readFileSync(WSDL_FILE, "utf8")
|
|
||||||
);
|
|
||||||
|
|
||||||
x.log = (type, data) => {
|
|
||||||
switch (type) {
|
|
||||||
case "info":
|
|
||||||
logger.info({ level: "info", data });
|
|
||||||
break;
|
|
||||||
case "warn":
|
|
||||||
logger.warn({ level: "warn", data });
|
|
||||||
break;
|
|
||||||
case "error":
|
|
||||||
logger.error({ level: "error", data });
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
logger.debug({ level: "debug", data });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|||||||
134
src/smapi.ts
134
src/smapi.ts
@@ -1,2 +1,134 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
import { Express } from "express";
|
||||||
|
import { listen } from "soap";
|
||||||
|
import { readFileSync } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import logger from "./logger";
|
||||||
|
|
||||||
export type GetAppLinkResult = { getAppLinkResult: { authorizeAccount: { appUrlStringId: string, deviceLink: { regUrl: string, linkCode:string,showLinkCode:boolean }} } }
|
import { LinkCodes } from "./link_codes";
|
||||||
|
|
||||||
|
export const LOGIN_ROUTE = "/login"
|
||||||
|
export const SOAP_PATH = "/ws/sonos";
|
||||||
|
export const STRINGS_ROUTE = "/sonos/strings.xml";
|
||||||
|
export const PRESENTATION_MAP_ROUTE = "/sonos/presentationMap.xml";
|
||||||
|
|
||||||
|
const WSDL_FILE = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"Sonoswsdl-1.19.4-20190411.142401-3.wsdl"
|
||||||
|
);
|
||||||
|
|
||||||
|
export type GetAppLinkResult = {
|
||||||
|
getAppLinkResult: {
|
||||||
|
authorizeAccount: {
|
||||||
|
appUrlStringId: string;
|
||||||
|
deviceLink: { regUrl: string; linkCode: string; showLinkCode: boolean };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetDeviceAuthTokenResult = {
|
||||||
|
getDeviceAuthTokenResult: {
|
||||||
|
authToken: string;
|
||||||
|
privateKey: string;
|
||||||
|
userInfo: {
|
||||||
|
nickname: string;
|
||||||
|
userIdHashCode: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
class SonosSoap {
|
||||||
|
linkCodes: LinkCodes;
|
||||||
|
webAddress: string;
|
||||||
|
|
||||||
|
constructor(webAddress: string, linkCodes: LinkCodes) {
|
||||||
|
this.webAddress = webAddress;
|
||||||
|
this.linkCodes = linkCodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAppLink(): GetAppLinkResult {
|
||||||
|
const linkCode = this.linkCodes.mint();
|
||||||
|
return {
|
||||||
|
getAppLinkResult: {
|
||||||
|
authorizeAccount: {
|
||||||
|
appUrlStringId: "AppLinkMessage",
|
||||||
|
deviceLink: {
|
||||||
|
regUrl: `${this.webAddress}${LOGIN_ROUTE}?linkCode=${linkCode}`,
|
||||||
|
linkCode: linkCode,
|
||||||
|
showLinkCode: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getDeviceAuthToken({
|
||||||
|
linkCode,
|
||||||
|
}: {
|
||||||
|
linkCode: string;
|
||||||
|
}): GetDeviceAuthTokenResult {
|
||||||
|
const association = this.linkCodes.associationFor(linkCode);
|
||||||
|
if (association) {
|
||||||
|
return {
|
||||||
|
getDeviceAuthTokenResult: {
|
||||||
|
authToken: association.authToken.value,
|
||||||
|
privateKey: association.authToken.version,
|
||||||
|
userInfo: {
|
||||||
|
nickname: association.nickname,
|
||||||
|
userIdHashCode: crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(association.userId)
|
||||||
|
.digest("hex"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw {
|
||||||
|
Fault: {
|
||||||
|
faultcode: "Client.NOT_LINKED_RETRY",
|
||||||
|
faultstring: "Link Code not found retry...",
|
||||||
|
detail: {
|
||||||
|
ExceptionInfo: "NOT_LINKED_RETRY",
|
||||||
|
SonosError: "5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindSmapiSoapServiceToExpress(app: Express, soapPath:string, webAddress: string, linkCodes: LinkCodes) {
|
||||||
|
const sonosSoap = new SonosSoap(webAddress, linkCodes);
|
||||||
|
const soapyService = listen(
|
||||||
|
app,
|
||||||
|
soapPath,
|
||||||
|
{
|
||||||
|
Sonos: {
|
||||||
|
SonosSoap: {
|
||||||
|
getAppLink: () => sonosSoap.getAppLink(),
|
||||||
|
getDeviceAuthToken: ({ linkCode }: { linkCode: string }) =>
|
||||||
|
sonosSoap.getDeviceAuthToken({ linkCode }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
readFileSync(WSDL_FILE, "utf8")
|
||||||
|
);
|
||||||
|
|
||||||
|
soapyService.log = (type, data) => {
|
||||||
|
switch (type) {
|
||||||
|
case "info":
|
||||||
|
logger.info({ level: "info", data });
|
||||||
|
break;
|
||||||
|
case "warn":
|
||||||
|
logger.warn({ level: "warn", data });
|
||||||
|
break;
|
||||||
|
case "error":
|
||||||
|
logger.error({ level: "error", data });
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logger.debug({ level: "debug", data });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default bindSmapiSoapServiceToExpress;
|
||||||
|
|||||||
@@ -5,10 +5,7 @@ import { MusicService } from "@svrooij/sonos/lib/services";
|
|||||||
import { head } from "underscore";
|
import { head } from "underscore";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import STRINGS from './strings';
|
import STRINGS from './strings';
|
||||||
|
import { SOAP_PATH, STRINGS_ROUTE, PRESENTATION_MAP_ROUTE } from './smapi';
|
||||||
export const SOAP_PATH = "/ws/sonos";
|
|
||||||
export const STRINGS_PATH = "/sonos/strings.xml";
|
|
||||||
export const PRESENTATION_MAP_PATH = "/sonos/presentationMap.xml";
|
|
||||||
|
|
||||||
export type Device = {
|
export type Device = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -42,11 +39,11 @@ export const bonobService = (
|
|||||||
uri: `${stripTailingSlash(bonobRoot)}${SOAP_PATH}`,
|
uri: `${stripTailingSlash(bonobRoot)}${SOAP_PATH}`,
|
||||||
secureUri: `${stripTailingSlash(bonobRoot)}${SOAP_PATH}`,
|
secureUri: `${stripTailingSlash(bonobRoot)}${SOAP_PATH}`,
|
||||||
strings: {
|
strings: {
|
||||||
uri: `${stripTailingSlash(bonobRoot)}${STRINGS_PATH}`,
|
uri: `${stripTailingSlash(bonobRoot)}${STRINGS_ROUTE}`,
|
||||||
version: STRINGS.version,
|
version: STRINGS.version,
|
||||||
},
|
},
|
||||||
presentation: {
|
presentation: {
|
||||||
uri: `${stripTailingSlash(bonobRoot)}${PRESENTATION_MAP_PATH}`,
|
uri: `${stripTailingSlash(bonobRoot)}${PRESENTATION_MAP_ROUTE}`,
|
||||||
version: "1",
|
version: "1",
|
||||||
},
|
},
|
||||||
pollInterval: 1200,
|
pollInterval: 1200,
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import * as xpath from "xpath-ts";
|
|||||||
|
|
||||||
import { InMemoryLinkCodes, LinkCodes } from "../src/link_codes";
|
import { InMemoryLinkCodes, LinkCodes } from "../src/link_codes";
|
||||||
import makeServer from "../src/server";
|
import makeServer from "../src/server";
|
||||||
import { bonobService, SONOS_DISABLED, STRINGS_PATH } from "../src/sonos";
|
import { bonobService, SONOS_DISABLED } from "../src/sonos";
|
||||||
|
import { STRINGS_ROUTE, LOGIN_ROUTE } from "../src/smapi";
|
||||||
|
|
||||||
import { aService, getAppLinkMessage } from "./builders";
|
import { aService, getAppLinkMessage } from "./builders";
|
||||||
import { InMemoryMusicService } from "./in_memory_music_service";
|
import { InMemoryMusicService } from "./in_memory_music_service";
|
||||||
@@ -26,7 +27,7 @@ describe("service config", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
it("should return xml for the strings", async () => {
|
it("should return xml for the strings", async () => {
|
||||||
const res = await request(server).get(STRINGS_PATH).send();
|
const res = await request(server).get(STRINGS_ROUTE).send();
|
||||||
|
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ describe("api", () => {
|
|||||||
linkCodes
|
linkCodes
|
||||||
);
|
);
|
||||||
|
|
||||||
describe("/login", () => {
|
describe(LOGIN_ROUTE, () => {
|
||||||
describe("when the credentials are valid", () => {
|
describe("when the credentials are valid", () => {
|
||||||
it("should return 200 ok and have associated linkCode with user", async () => {
|
it("should return 200 ok and have associated linkCode with user", async () => {
|
||||||
const username = "jane";
|
const username = "jane";
|
||||||
@@ -71,7 +72,7 @@ describe("api", () => {
|
|||||||
musicService.hasUser({ username, password });
|
musicService.hasUser({ username, password });
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.post("/login")
|
.post(LOGIN_ROUTE)
|
||||||
.type("form")
|
.type("form")
|
||||||
.send({ username, password, linkCode })
|
.send({ username, password, linkCode })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
@@ -92,7 +93,7 @@ describe("api", () => {
|
|||||||
musicService.hasNoUsers();
|
musicService.hasNoUsers();
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.post("/login")
|
.post(LOGIN_ROUTE)
|
||||||
.type("form")
|
.type("form")
|
||||||
.send({ username, password, linkCode })
|
.send({ username, password, linkCode })
|
||||||
.expect(403);
|
.expect(403);
|
||||||
@@ -110,7 +111,7 @@ describe("api", () => {
|
|||||||
musicService.hasUser({ username, password });
|
musicService.hasUser({ username, password });
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.post("/login")
|
.post(LOGIN_ROUTE)
|
||||||
.type("form")
|
.type("form")
|
||||||
.send({ username, password, linkCode })
|
.send({ username, password, linkCode })
|
||||||
.expect(400);
|
.expect(400);
|
||||||
@@ -160,7 +161,6 @@ describe("api", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe("getDeviceAuthToken", () => {
|
describe("getDeviceAuthToken", () => {
|
||||||
const linkCodes = new InMemoryLinkCodes();
|
const linkCodes = new InMemoryLinkCodes();
|
||||||
@@ -225,4 +225,7 @@ describe("api", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import makeServer from "../src/server";
|
|
||||||
import { SONOS_DISABLED, SOAP_PATH } from "../src/sonos";
|
|
||||||
|
|
||||||
import { aService } from "./builders";
|
|
||||||
import { InMemoryMusicService } from "./in_memory_music_service";
|
|
||||||
import supersoap from './supersoap';
|
|
||||||
|
|
||||||
import { createClientAsync } from "soap";
|
|
||||||
|
|
||||||
describe("ws", () => {
|
|
||||||
describe("can call getSessionId", () => {
|
|
||||||
it("should do something", async () => {
|
|
||||||
const WEB_ADDRESS = 'http://localhost:7653'
|
|
||||||
const server = makeServer(SONOS_DISABLED, aService(), WEB_ADDRESS, new InMemoryMusicService());
|
|
||||||
|
|
||||||
const { username, sessionId } = await createClientAsync(
|
|
||||||
`${WEB_ADDRESS}${SOAP_PATH}?wsdl`,
|
|
||||||
{
|
|
||||||
endpoint: `${WEB_ADDRESS}${SOAP_PATH}`,
|
|
||||||
httpClient: supersoap(server, WEB_ADDRESS),
|
|
||||||
}
|
|
||||||
).then((client) =>
|
|
||||||
client
|
|
||||||
.getSessionIdAsync({ username: "bob", password: "foo" })
|
|
||||||
.then(
|
|
||||||
([{ username, sessionId }]: [
|
|
||||||
{ username: string; sessionId: string }
|
|
||||||
]) => ({
|
|
||||||
username,
|
|
||||||
sessionId,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(username).toEqual("bob");
|
|
||||||
expect(sessionId).toEqual("123");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<div id="content">
|
<div id="content">
|
||||||
<h1>Log in to <%= it.bonobService.name %></h1>
|
<h1>Log in to <%= it.bonobService.name %></h1>
|
||||||
<form action="/login" method="POST">
|
<form action="<%= it.loginRoute %>" method="POST">
|
||||||
<label for="username">Username:</label><input type="text" id="username" name="username"><br>
|
<label for="username">Username:</label><input type="text" id="username" name="username"><br>
|
||||||
<label for="password">Password:</label><input type="text" id="password" name="password"><br>
|
<label for="password">Password:</label><input type="text" id="password" name="password"><br>
|
||||||
<input type="hidden" name="linkCode" value="<%= it.linkCode %>">
|
<input type="hidden" name="linkCode" value="<%= it.linkCode %>">
|
||||||
|
|||||||
Reference in New Issue
Block a user