mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Make Smapi responsible for turning app token into encrypted jwt (#71)
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
"@svrooij/sonos": "^2.4.0",
|
"@svrooij/sonos": "^2.4.0",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@types/fs-extra": "^9.0.13",
|
"@types/fs-extra": "^9.0.13",
|
||||||
|
"@types/jsonwebtoken": "^8.5.5",
|
||||||
"@types/morgan": "^1.9.3",
|
"@types/morgan": "^1.9.3",
|
||||||
"@types/node": "^16.7.13",
|
"@types/node": "^16.7.13",
|
||||||
"@types/sharp": "^0.28.6",
|
"@types/sharp": "^0.28.6",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"fp-ts": "^2.11.1",
|
"fp-ts": "^2.11.1",
|
||||||
"fs-extra": "^10.0.0",
|
"fs-extra": "^10.0.0",
|
||||||
|
"jsonwebtoken": "^8.5.1",
|
||||||
"libxmljs2": "^0.28.0",
|
"libxmljs2": "^0.28.0",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"node-html-parser": "^4.1.4",
|
"node-html-parser": "^4.1.4",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import path from "path";
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import server from "./server";
|
import server from "./server";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
appendMimeTypeToClientFor,
|
appendMimeTypeToClientFor,
|
||||||
axiosImageFetcher,
|
axiosImageFetcher,
|
||||||
@@ -9,13 +10,13 @@ import {
|
|||||||
DEFAULT,
|
DEFAULT,
|
||||||
Subsonic,
|
Subsonic,
|
||||||
} from "./subsonic";
|
} from "./subsonic";
|
||||||
import encryption from "./encryption";
|
|
||||||
import { InMemoryAccessTokens, sha256 } from "./access_tokens";
|
import { InMemoryAccessTokens, sha256 } from "./access_tokens";
|
||||||
import { InMemoryLinkCodes } from "./link_codes";
|
import { InMemoryLinkCodes } from "./link_codes";
|
||||||
import readConfig from "./config";
|
import readConfig from "./config";
|
||||||
import sonos, { bonobService } from "./sonos";
|
import sonos, { bonobService } from "./sonos";
|
||||||
import { MusicService } from "./music_service";
|
import { MusicService } from "./music_service";
|
||||||
import { SystemClock } from "./clock";
|
import { SystemClock } from "./clock";
|
||||||
|
import { jwtTokenSigner } from "./encryption";
|
||||||
|
|
||||||
const config = readConfig();
|
const config = readConfig();
|
||||||
|
|
||||||
@@ -40,7 +41,6 @@ const artistImageFetcher = config.subsonic.artistImageCache
|
|||||||
|
|
||||||
const subsonic = new Subsonic(
|
const subsonic = new Subsonic(
|
||||||
config.subsonic.url,
|
config.subsonic.url,
|
||||||
encryption(config.secret),
|
|
||||||
streamUserAgent,
|
streamUserAgent,
|
||||||
artistImageFetcher
|
artistImageFetcher
|
||||||
);
|
);
|
||||||
@@ -88,6 +88,7 @@ const app = server(
|
|||||||
applyContextPath: true,
|
applyContextPath: true,
|
||||||
logRequests: true,
|
logRequests: true,
|
||||||
version,
|
version,
|
||||||
|
tokenSigner: jwtTokenSigner(config.secret)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,89 @@
|
|||||||
import { createCipheriv, createDecipheriv, randomBytes, createHash } from "crypto";
|
import {
|
||||||
|
createCipheriv,
|
||||||
|
createDecipheriv,
|
||||||
|
randomBytes,
|
||||||
|
createHash,
|
||||||
|
} from "crypto";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
const ALGORITHM = "aes-256-cbc"
|
const ALGORITHM = "aes-256-cbc";
|
||||||
const IV = randomBytes(16);
|
const IV = randomBytes(16);
|
||||||
|
|
||||||
|
export type Signer = {
|
||||||
|
sign: (value: string) => string;
|
||||||
|
verify: (token: string) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pSigner = (signer: Signer) => ({
|
||||||
|
sign: (value: string): Promise<string> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
return resolve(signer.sign(value));
|
||||||
|
} catch(e) {
|
||||||
|
reject(`Failed to sign value: ${e}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
verify: (token: string): Promise<string> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
return resolve(signer.verify(token));
|
||||||
|
}catch(e) {
|
||||||
|
reject(`Failed to verify value: ${e}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const jwtTokenSigner = (secret: string) => ({
|
||||||
|
sign: (value: string) => jwt.sign(value, secret),
|
||||||
|
verify: (token: string) => {
|
||||||
|
try {
|
||||||
|
return jwt.verify(token, secret) as string;
|
||||||
|
} catch (e) {
|
||||||
|
throw `Failed to decode jwt, try re-authorising account`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export type Hash = {
|
export type Hash = {
|
||||||
iv: string,
|
iv: string;
|
||||||
encryptedData: string
|
encryptedData: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type Encryption = {
|
export type Encryption = {
|
||||||
encrypt: (value:string) => Hash
|
encrypt: (value: string) => Hash;
|
||||||
decrypt: (hash: Hash) => string
|
decrypt: (hash: Hash) => string;
|
||||||
}
|
};
|
||||||
|
|
||||||
const encryption = (secret: string): Encryption => {
|
const encryption = (secret: string): Encryption => {
|
||||||
const key = createHash('sha256').update(String(secret)).digest('base64').substr(0, 32);
|
const key = createHash("sha256")
|
||||||
|
.update(String(secret))
|
||||||
|
.digest("base64")
|
||||||
|
.substr(0, 32);
|
||||||
return {
|
return {
|
||||||
encrypt: (value: string) => {
|
encrypt: (value: string) => {
|
||||||
const cipher = createCipheriv(ALGORITHM, key, IV);
|
const cipher = createCipheriv(ALGORITHM, key, IV);
|
||||||
return {
|
return {
|
||||||
iv: IV.toString("hex"),
|
iv: IV.toString("hex"),
|
||||||
encryptedData: Buffer.concat([cipher.update(value), cipher.final()]).toString("hex")
|
encryptedData: Buffer.concat([
|
||||||
|
cipher.update(value),
|
||||||
|
cipher.final(),
|
||||||
|
]).toString("hex"),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
decrypt: (hash: Hash) => {
|
decrypt: (hash: Hash) => {
|
||||||
const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(hash.iv, 'hex'));
|
const decipher = createDecipheriv(
|
||||||
return Buffer.concat([decipher.update(Buffer.from(hash.encryptedData, 'hex')), decipher.final()]).toString();
|
ALGORITHM,
|
||||||
}
|
key,
|
||||||
}
|
Buffer.from(hash.iv, "hex")
|
||||||
}
|
);
|
||||||
|
return Buffer.concat([
|
||||||
|
decipher.update(Buffer.from(hash.encryptedData, "hex")),
|
||||||
|
decipher.final(),
|
||||||
|
]).toString();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default encryption;
|
export default encryption;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { Icon, ICONS, festivals, features } from "./icon";
|
|||||||
import _, { shuffle } from "underscore";
|
import _, { shuffle } from "underscore";
|
||||||
import morgan from "morgan";
|
import morgan from "morgan";
|
||||||
import { takeWithRepeats } from "./utils";
|
import { takeWithRepeats } from "./utils";
|
||||||
|
import { jwtTokenSigner, Signer } from "./encryption";
|
||||||
|
|
||||||
export const BONOB_ACCESS_TOKEN_HEADER = "bat";
|
export const BONOB_ACCESS_TOKEN_HEADER = "bat";
|
||||||
|
|
||||||
@@ -85,6 +86,7 @@ export type ServerOpts = {
|
|||||||
applyContextPath: boolean;
|
applyContextPath: boolean;
|
||||||
logRequests: boolean;
|
logRequests: boolean;
|
||||||
version: string;
|
version: string;
|
||||||
|
tokenSigner: Signer;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_SERVER_OPTS: ServerOpts = {
|
const DEFAULT_SERVER_OPTS: ServerOpts = {
|
||||||
@@ -95,6 +97,7 @@ const DEFAULT_SERVER_OPTS: ServerOpts = {
|
|||||||
applyContextPath: true,
|
applyContextPath: true,
|
||||||
logRequests: false,
|
logRequests: false,
|
||||||
version: "v?",
|
version: "v?",
|
||||||
|
tokenSigner: jwtTokenSigner(`bonob-${uuid()}`),
|
||||||
};
|
};
|
||||||
|
|
||||||
function server(
|
function server(
|
||||||
@@ -585,7 +588,8 @@ function server(
|
|||||||
musicService,
|
musicService,
|
||||||
accessTokens,
|
accessTokens,
|
||||||
clock,
|
clock,
|
||||||
i8n
|
i8n,
|
||||||
|
serverOpts.tokenSigner
|
||||||
);
|
);
|
||||||
|
|
||||||
if (serverOpts.applyContextPath) {
|
if (serverOpts.applyContextPath) {
|
||||||
|
|||||||
93
src/smapi.ts
93
src/smapi.ts
@@ -24,6 +24,7 @@ import { URLBuilder } from "./url_builder";
|
|||||||
import { asLANGs, I8N } from "./i8n";
|
import { asLANGs, I8N } from "./i8n";
|
||||||
import { ICON, iconForGenre } from "./icon";
|
import { ICON, iconForGenre } from "./icon";
|
||||||
import { uniq } from "underscore";
|
import { uniq } from "underscore";
|
||||||
|
import { pSigner, Signer } from "./encryption";
|
||||||
|
|
||||||
export const LOGIN_ROUTE = "/login";
|
export const LOGIN_ROUTE = "/login";
|
||||||
export const CREATE_REGISTRATION_ROUTE = "/registration/add";
|
export const CREATE_REGISTRATION_ROUTE = "/registration/add";
|
||||||
@@ -145,10 +146,12 @@ export function searchResult(
|
|||||||
class SonosSoap {
|
class SonosSoap {
|
||||||
linkCodes: LinkCodes;
|
linkCodes: LinkCodes;
|
||||||
bonobUrl: URLBuilder;
|
bonobUrl: URLBuilder;
|
||||||
|
tokenSigner: Signer
|
||||||
|
|
||||||
constructor(bonobUrl: URLBuilder, linkCodes: LinkCodes) {
|
constructor(bonobUrl: URLBuilder, linkCodes: LinkCodes, tokenSigner: Signer) {
|
||||||
this.bonobUrl = bonobUrl;
|
this.bonobUrl = bonobUrl;
|
||||||
this.linkCodes = linkCodes;
|
this.linkCodes = linkCodes;
|
||||||
|
this.tokenSigner = tokenSigner
|
||||||
}
|
}
|
||||||
|
|
||||||
getAppLink(): GetAppLinkResult {
|
getAppLink(): GetAppLinkResult {
|
||||||
@@ -179,7 +182,7 @@ class SonosSoap {
|
|||||||
if (association) {
|
if (association) {
|
||||||
return {
|
return {
|
||||||
getDeviceAuthTokenResult: {
|
getDeviceAuthTokenResult: {
|
||||||
authToken: association.authToken,
|
authToken: this.tokenSigner.sign(association.authToken),
|
||||||
privateKey: "",
|
privateKey: "",
|
||||||
userInfo: {
|
userInfo: {
|
||||||
nickname: association.nickname,
|
nickname: association.nickname,
|
||||||
@@ -321,39 +324,6 @@ export const artist = (bonobUrl: URLBuilder, artist: ArtistSummary) => ({
|
|||||||
albumArtURI: defaultArtistArtURI(bonobUrl, artist).href(),
|
albumArtURI: defaultArtistArtURI(bonobUrl, artist).href(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const auth = async (
|
|
||||||
musicService: MusicService,
|
|
||||||
accessTokens: AccessTokens,
|
|
||||||
credentials?: Credentials
|
|
||||||
) => {
|
|
||||||
if (!credentials) {
|
|
||||||
throw {
|
|
||||||
Fault: {
|
|
||||||
faultcode: "Client.LoginUnsupported",
|
|
||||||
faultstring: "Missing credentials...",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const authToken = credentials.loginToken.token;
|
|
||||||
const accessToken = accessTokens.mint(authToken);
|
|
||||||
|
|
||||||
return musicService
|
|
||||||
.login(authToken)
|
|
||||||
.then((musicLibrary) => ({
|
|
||||||
musicLibrary,
|
|
||||||
authToken,
|
|
||||||
accessToken,
|
|
||||||
}))
|
|
||||||
.catch((_) => {
|
|
||||||
throw {
|
|
||||||
Fault: {
|
|
||||||
faultcode: "Client.LoginUnauthorized",
|
|
||||||
faultstring: "Credentials not found...",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function splitId<T>(id: string) {
|
function splitId<T>(id: string) {
|
||||||
const [type, typeId] = id.split(":");
|
const [type, typeId] = id.split(":");
|
||||||
return (t: T) => ({
|
return (t: T) => ({
|
||||||
@@ -375,9 +345,10 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
musicService: MusicService,
|
musicService: MusicService,
|
||||||
accessTokens: AccessTokens,
|
accessTokens: AccessTokens,
|
||||||
clock: Clock,
|
clock: Clock,
|
||||||
i8n: I8N
|
i8n: I8N,
|
||||||
|
tokenSigner: Signer,
|
||||||
) {
|
) {
|
||||||
const sonosSoap = new SonosSoap(bonobUrl, linkCodes);
|
const sonosSoap = new SonosSoap(bonobUrl, linkCodes, tokenSigner);
|
||||||
|
|
||||||
const urlWithToken = (accessToken: string) =>
|
const urlWithToken = (accessToken: string) =>
|
||||||
bonobUrl.append({
|
bonobUrl.append({
|
||||||
@@ -386,6 +357,32 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const auth = async (
|
||||||
|
credentials?: Credentials
|
||||||
|
) => {
|
||||||
|
if (!credentials) {
|
||||||
|
throw {
|
||||||
|
Fault: {
|
||||||
|
faultcode: "Client.LoginUnsupported",
|
||||||
|
faultstring: "Missing credentials...",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return pSigner(tokenSigner)
|
||||||
|
.verify(credentials.loginToken.token)
|
||||||
|
.then(authToken => ({ authToken, accessToken: accessTokens.mint(authToken) }))
|
||||||
|
.then((tokens) => musicService.login(tokens.authToken).then(musicLibrary => ({ ...tokens, musicLibrary })))
|
||||||
|
.catch((_) => {
|
||||||
|
throw {
|
||||||
|
Fault: {
|
||||||
|
faultcode: "Client.LoginUnauthorized",
|
||||||
|
faultstring: "Failed to authenticate, try Reauthorising your account in the sonos app",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const soapyService = listen(
|
const soapyService = listen(
|
||||||
app,
|
app,
|
||||||
soapPath,
|
soapPath,
|
||||||
@@ -408,7 +405,7 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
_,
|
_,
|
||||||
soapyHeaders: SoapyHeaders
|
soapyHeaders: SoapyHeaders
|
||||||
) =>
|
) =>
|
||||||
auth(musicService, accessTokens, soapyHeaders?.credentials)
|
auth(soapyHeaders?.credentials)
|
||||||
.then(splitId(id))
|
.then(splitId(id))
|
||||||
.then(({ accessToken, type, typeId }) => ({
|
.then(({ accessToken, type, typeId }) => ({
|
||||||
getMediaURIResult: bonobUrl
|
getMediaURIResult: bonobUrl
|
||||||
@@ -423,7 +420,7 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
_,
|
_,
|
||||||
soapyHeaders: SoapyHeaders
|
soapyHeaders: SoapyHeaders
|
||||||
) =>
|
) =>
|
||||||
auth(musicService, accessTokens, soapyHeaders?.credentials)
|
auth(soapyHeaders?.credentials)
|
||||||
.then(splitId(id))
|
.then(splitId(id))
|
||||||
.then(async ({ musicLibrary, accessToken, typeId }) =>
|
.then(async ({ musicLibrary, accessToken, typeId }) =>
|
||||||
musicLibrary.track(typeId!).then((it) => ({
|
musicLibrary.track(typeId!).then((it) => ({
|
||||||
@@ -435,7 +432,7 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
_,
|
_,
|
||||||
soapyHeaders: SoapyHeaders
|
soapyHeaders: SoapyHeaders
|
||||||
) =>
|
) =>
|
||||||
auth(musicService, accessTokens, soapyHeaders?.credentials)
|
auth(soapyHeaders?.credentials)
|
||||||
.then(splitId(id))
|
.then(splitId(id))
|
||||||
.then(async ({ musicLibrary, accessToken }) => {
|
.then(async ({ musicLibrary, accessToken }) => {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
@@ -480,7 +477,7 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
_,
|
_,
|
||||||
soapyHeaders: SoapyHeaders
|
soapyHeaders: SoapyHeaders
|
||||||
) =>
|
) =>
|
||||||
auth(musicService, accessTokens, soapyHeaders?.credentials)
|
auth(soapyHeaders?.credentials)
|
||||||
.then(splitId(id))
|
.then(splitId(id))
|
||||||
.then(async ({ musicLibrary, accessToken, type, typeId }) => {
|
.then(async ({ musicLibrary, accessToken, type, typeId }) => {
|
||||||
const paging = { _index: index, _count: count };
|
const paging = { _index: index, _count: count };
|
||||||
@@ -552,7 +549,7 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
soapyHeaders: SoapyHeaders,
|
soapyHeaders: SoapyHeaders,
|
||||||
{ headers }: Pick<Request, "headers">
|
{ headers }: Pick<Request, "headers">
|
||||||
) =>
|
) =>
|
||||||
auth(musicService, accessTokens, soapyHeaders?.credentials)
|
auth(soapyHeaders?.credentials)
|
||||||
.then(splitId(id))
|
.then(splitId(id))
|
||||||
.then(({ musicLibrary, accessToken, type, typeId }) => {
|
.then(({ musicLibrary, accessToken, type, typeId }) => {
|
||||||
const paging = { _index: index, _count: count };
|
const paging = { _index: index, _count: count };
|
||||||
@@ -825,7 +822,7 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
_,
|
_,
|
||||||
soapyHeaders: SoapyHeaders
|
soapyHeaders: SoapyHeaders
|
||||||
) =>
|
) =>
|
||||||
auth(musicService, accessTokens, soapyHeaders?.credentials)
|
auth(soapyHeaders?.credentials)
|
||||||
.then(({ musicLibrary }) =>
|
.then(({ musicLibrary }) =>
|
||||||
musicLibrary
|
musicLibrary
|
||||||
.createPlaylist(title)
|
.createPlaylist(title)
|
||||||
@@ -851,7 +848,7 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
_,
|
_,
|
||||||
soapyHeaders: SoapyHeaders
|
soapyHeaders: SoapyHeaders
|
||||||
) =>
|
) =>
|
||||||
auth(musicService, accessTokens, soapyHeaders?.credentials)
|
auth(soapyHeaders?.credentials)
|
||||||
.then(({ musicLibrary }) => musicLibrary.deletePlaylist(id))
|
.then(({ musicLibrary }) => musicLibrary.deletePlaylist(id))
|
||||||
.then((_) => ({ deleteContainerResult: {} })),
|
.then((_) => ({ deleteContainerResult: {} })),
|
||||||
addToContainer: async (
|
addToContainer: async (
|
||||||
@@ -859,7 +856,7 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
_,
|
_,
|
||||||
soapyHeaders: SoapyHeaders
|
soapyHeaders: SoapyHeaders
|
||||||
) =>
|
) =>
|
||||||
auth(musicService, accessTokens, soapyHeaders?.credentials)
|
auth(soapyHeaders?.credentials)
|
||||||
.then(splitId(id))
|
.then(splitId(id))
|
||||||
.then(({ musicLibrary, typeId }) =>
|
.then(({ musicLibrary, typeId }) =>
|
||||||
musicLibrary.addToPlaylist(parentId.split(":")[1]!, typeId)
|
musicLibrary.addToPlaylist(parentId.split(":")[1]!, typeId)
|
||||||
@@ -870,7 +867,7 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
_,
|
_,
|
||||||
soapyHeaders: SoapyHeaders
|
soapyHeaders: SoapyHeaders
|
||||||
) =>
|
) =>
|
||||||
auth(musicService, accessTokens, soapyHeaders?.credentials)
|
auth(soapyHeaders?.credentials)
|
||||||
.then(splitId(id))
|
.then(splitId(id))
|
||||||
.then((it) => ({
|
.then((it) => ({
|
||||||
...it,
|
...it,
|
||||||
@@ -893,7 +890,7 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
_,
|
_,
|
||||||
soapyHeaders: SoapyHeaders
|
soapyHeaders: SoapyHeaders
|
||||||
) =>
|
) =>
|
||||||
auth(musicService, accessTokens, soapyHeaders?.credentials)
|
auth(soapyHeaders?.credentials)
|
||||||
.then(splitId(id))
|
.then(splitId(id))
|
||||||
.then(({ musicLibrary, typeId }) =>
|
.then(({ musicLibrary, typeId }) =>
|
||||||
musicLibrary.rate(typeId, ratingFromInt(Math.abs(rating)))
|
musicLibrary.rate(typeId, ratingFromInt(Math.abs(rating)))
|
||||||
@@ -905,7 +902,7 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
_,
|
_,
|
||||||
soapyHeaders: SoapyHeaders
|
soapyHeaders: SoapyHeaders
|
||||||
) =>
|
) =>
|
||||||
auth(musicService, accessTokens, soapyHeaders?.credentials)
|
auth(soapyHeaders?.credentials)
|
||||||
.then(splitId(id))
|
.then(splitId(id))
|
||||||
.then(({ musicLibrary, type, typeId }) => {
|
.then(({ musicLibrary, type, typeId }) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import fse from "fs-extra";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
import axios, { AxiosRequestConfig } from "axios";
|
import axios, { AxiosRequestConfig } from "axios";
|
||||||
import { Encryption } from "./encryption";
|
|
||||||
import randomString from "./random_string";
|
import randomString from "./random_string";
|
||||||
import { b64Encode, b64Decode } from "./b64";
|
import { b64Encode, b64Decode } from "./b64";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
@@ -369,18 +368,15 @@ const AlbumQueryTypeToSubsonicType: Record<AlbumQueryType, string> = {
|
|||||||
|
|
||||||
export class Subsonic implements MusicService {
|
export class Subsonic implements MusicService {
|
||||||
url: string;
|
url: string;
|
||||||
encryption: Encryption;
|
|
||||||
streamClientApplication: StreamClientApplication;
|
streamClientApplication: StreamClientApplication;
|
||||||
externalImageFetcher: ImageFetcher;
|
externalImageFetcher: ImageFetcher;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
url: string,
|
url: string,
|
||||||
encryption: Encryption,
|
|
||||||
streamClientApplication: StreamClientApplication = DEFAULT,
|
streamClientApplication: StreamClientApplication = DEFAULT,
|
||||||
externalImageFetcher: ImageFetcher = axiosImageFetcher
|
externalImageFetcher: ImageFetcher = axiosImageFetcher
|
||||||
) {
|
) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.encryption = encryption;
|
|
||||||
this.streamClientApplication = streamClientApplication;
|
this.streamClientApplication = streamClientApplication;
|
||||||
this.externalImageFetcher = externalImageFetcher;
|
this.externalImageFetcher = externalImageFetcher;
|
||||||
}
|
}
|
||||||
@@ -428,15 +424,14 @@ export class Subsonic implements MusicService {
|
|||||||
this.getJSON(credentials, "/rest/ping.view")
|
this.getJSON(credentials, "/rest/ping.view")
|
||||||
.then(() => ({
|
.then(() => ({
|
||||||
authToken: b64Encode(
|
authToken: b64Encode(
|
||||||
JSON.stringify(this.encryption.encrypt(JSON.stringify(credentials)))
|
JSON.stringify(credentials)
|
||||||
),
|
),
|
||||||
userId: credentials.username,
|
userId: credentials.username,
|
||||||
nickname: credentials.username,
|
nickname: credentials.username,
|
||||||
}))
|
}))
|
||||||
.catch((e) => ({ message: `${e}` }));
|
.catch((e) => ({ message: `${e}` }));
|
||||||
|
|
||||||
parseToken = (token: string): Credentials =>
|
parseToken = (token: string): Credentials => JSON.parse(b64Decode(token));
|
||||||
JSON.parse(this.encryption.decrypt(JSON.parse(b64Decode(token))));
|
|
||||||
|
|
||||||
getArtists = (
|
getArtists = (
|
||||||
credentials: Credentials
|
credentials: Credentials
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SonosDevice } from "@svrooij/sonos/lib";
|
import { SonosDevice } from "@svrooij/sonos/lib";
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
import { Credentials } from "../src/smapi";
|
|
||||||
|
|
||||||
|
import { Credentials } from "../src/smapi";
|
||||||
import { Service, Device } from "../src/sonos";
|
import { Service, Device } from "../src/sonos";
|
||||||
import {
|
import {
|
||||||
Album,
|
Album,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,6 @@ import {
|
|||||||
cachingImageFetcher,
|
cachingImageFetcher,
|
||||||
asTrack,
|
asTrack,
|
||||||
} from "../src/subsonic";
|
} from "../src/subsonic";
|
||||||
import encryption from "../src/encryption";
|
|
||||||
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
jest.mock("axios");
|
jest.mock("axios");
|
||||||
@@ -593,7 +592,6 @@ describe("Subsonic", () => {
|
|||||||
const streamClientApplication = jest.fn();
|
const streamClientApplication = jest.fn();
|
||||||
const navidrome = new Subsonic(
|
const navidrome = new Subsonic(
|
||||||
url,
|
url,
|
||||||
encryption("secret"),
|
|
||||||
streamClientApplication
|
streamClientApplication
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
117
yarn.lock
117
yarn.lock
@@ -1184,6 +1184,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/jsonwebtoken@npm:^8.5.5":
|
||||||
|
version: 8.5.5
|
||||||
|
resolution: "@types/jsonwebtoken@npm:8.5.5"
|
||||||
|
dependencies:
|
||||||
|
"@types/node": "*"
|
||||||
|
checksum: 33c30354641bc7849be7507e9f48685b1f487e944321a932650eac6c247c85184667f5e207ccfcab0da8cb24bde93a8372c09cacf1849e976bbf2cb90b26ce90
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/keyv@npm:^3.1.1":
|
"@types/keyv@npm:^3.1.1":
|
||||||
version: 3.1.2
|
version: 3.1.2
|
||||||
resolution: "@types/keyv@npm:3.1.2"
|
resolution: "@types/keyv@npm:3.1.2"
|
||||||
@@ -1782,6 +1791,7 @@ __metadata:
|
|||||||
"@types/express": ^4.17.13
|
"@types/express": ^4.17.13
|
||||||
"@types/fs-extra": ^9.0.13
|
"@types/fs-extra": ^9.0.13
|
||||||
"@types/jest": ^27.0.1
|
"@types/jest": ^27.0.1
|
||||||
|
"@types/jsonwebtoken": ^8.5.5
|
||||||
"@types/mocha": ^9.0.0
|
"@types/mocha": ^9.0.0
|
||||||
"@types/morgan": ^1.9.3
|
"@types/morgan": ^1.9.3
|
||||||
"@types/node": ^16.7.13
|
"@types/node": ^16.7.13
|
||||||
@@ -1800,6 +1810,7 @@ __metadata:
|
|||||||
get-port: ^5.1.1
|
get-port: ^5.1.1
|
||||||
image-js: ^0.33.0
|
image-js: ^0.33.0
|
||||||
jest: ^27.1.0
|
jest: ^27.1.0
|
||||||
|
jsonwebtoken: ^8.5.1
|
||||||
libxmljs2: ^0.28.0
|
libxmljs2: ^0.28.0
|
||||||
morgan: ^1.10.0
|
morgan: ^1.10.0
|
||||||
node-html-parser: ^4.1.4
|
node-html-parser: ^4.1.4
|
||||||
@@ -1903,6 +1914,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"buffer-equal-constant-time@npm:1.0.1":
|
||||||
|
version: 1.0.1
|
||||||
|
resolution: "buffer-equal-constant-time@npm:1.0.1"
|
||||||
|
checksum: 80bb945f5d782a56f374b292770901065bad21420e34936ecbe949e57724b4a13874f735850dd1cc61f078773c4fb5493a41391e7bda40d1fa388d6bd80daaab
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"buffer-from@npm:^1.0.0":
|
"buffer-from@npm:^1.0.0":
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
resolution: "buffer-from@npm:1.1.1"
|
resolution: "buffer-from@npm:1.1.1"
|
||||||
@@ -2717,6 +2735,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"ecdsa-sig-formatter@npm:1.0.11":
|
||||||
|
version: 1.0.11
|
||||||
|
resolution: "ecdsa-sig-formatter@npm:1.0.11"
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: ^5.0.1
|
||||||
|
checksum: 207f9ab1c2669b8e65540bce29506134613dd5f122cccf1e6a560f4d63f2732d427d938f8481df175505aad94583bcb32c688737bb39a6df0625f903d6d93c03
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"ee-first@npm:1.1.1":
|
"ee-first@npm:1.1.1":
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
resolution: "ee-first@npm:1.1.1"
|
resolution: "ee-first@npm:1.1.1"
|
||||||
@@ -4641,6 +4668,45 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"jsonwebtoken@npm:^8.5.1":
|
||||||
|
version: 8.5.1
|
||||||
|
resolution: "jsonwebtoken@npm:8.5.1"
|
||||||
|
dependencies:
|
||||||
|
jws: ^3.2.2
|
||||||
|
lodash.includes: ^4.3.0
|
||||||
|
lodash.isboolean: ^3.0.3
|
||||||
|
lodash.isinteger: ^4.0.4
|
||||||
|
lodash.isnumber: ^3.0.3
|
||||||
|
lodash.isplainobject: ^4.0.6
|
||||||
|
lodash.isstring: ^4.0.1
|
||||||
|
lodash.once: ^4.0.0
|
||||||
|
ms: ^2.1.1
|
||||||
|
semver: ^5.6.0
|
||||||
|
checksum: 93c9e3f23c59b758ac88ba15f4e4753b3749dfce7a6f7c40fb86663128a1e282db085eec852d4e0cbca4cefdcd3a8275ee255dbd08fcad0df26ad9f6e4cc853a
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"jwa@npm:^1.4.1":
|
||||||
|
version: 1.4.1
|
||||||
|
resolution: "jwa@npm:1.4.1"
|
||||||
|
dependencies:
|
||||||
|
buffer-equal-constant-time: 1.0.1
|
||||||
|
ecdsa-sig-formatter: 1.0.11
|
||||||
|
safe-buffer: ^5.0.1
|
||||||
|
checksum: ff30ea7c2dcc61f3ed2098d868bf89d43701605090c5b21b5544b512843ec6fd9e028381a4dda466cbcdb885c2d1150f7c62e7168394ee07941b4098e1035e2f
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"jws@npm:^3.2.2":
|
||||||
|
version: 3.2.2
|
||||||
|
resolution: "jws@npm:3.2.2"
|
||||||
|
dependencies:
|
||||||
|
jwa: ^1.4.1
|
||||||
|
safe-buffer: ^5.0.1
|
||||||
|
checksum: f0213fe5b79344c56cd443428d8f65c16bf842dc8cb8f5aed693e1e91d79c20741663ad6eff07a6d2c433d1831acc9814e8d7bada6a0471fbb91d09ceb2bf5c2
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"keyv@npm:^3.0.0":
|
"keyv@npm:^3.0.0":
|
||||||
version: 3.1.0
|
version: 3.1.0
|
||||||
resolution: "keyv@npm:3.1.0"
|
resolution: "keyv@npm:3.1.0"
|
||||||
@@ -4710,6 +4776,55 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"lodash.includes@npm:^4.3.0":
|
||||||
|
version: 4.3.0
|
||||||
|
resolution: "lodash.includes@npm:4.3.0"
|
||||||
|
checksum: 71092c130515a67ab3bd928f57f6018434797c94def7f46aafa417771e455ce3a4834889f4267b17887d7f75297dfabd96231bf704fd2b8c5096dc4a913568b6
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"lodash.isboolean@npm:^3.0.3":
|
||||||
|
version: 3.0.3
|
||||||
|
resolution: "lodash.isboolean@npm:3.0.3"
|
||||||
|
checksum: b70068b4a8b8837912b54052557b21fc4774174e3512ed3c5b94621e5aff5eb6c68089d0a386b7e801d679cd105d2e35417978a5e99071750aa2ed90bffd0250
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"lodash.isinteger@npm:^4.0.4":
|
||||||
|
version: 4.0.4
|
||||||
|
resolution: "lodash.isinteger@npm:4.0.4"
|
||||||
|
checksum: 6034821b3fc61a2ffc34e7d5644bb50c5fd8f1c0121c554c21ac271911ee0c0502274852845005f8651d51e199ee2e0cfebfe40aaa49c7fe617f603a8a0b1691
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"lodash.isnumber@npm:^3.0.3":
|
||||||
|
version: 3.0.3
|
||||||
|
resolution: "lodash.isnumber@npm:3.0.3"
|
||||||
|
checksum: 913784275b565346255e6ae6a6e30b760a0da70abc29f3e1f409081585875105138cda4a429ff02577e1bc0a7ae2a90e0a3079a37f3a04c3d6c5aaa532f4cab2
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"lodash.isplainobject@npm:^4.0.6":
|
||||||
|
version: 4.0.6
|
||||||
|
resolution: "lodash.isplainobject@npm:4.0.6"
|
||||||
|
checksum: 29c6351f281e0d9a1d58f1a4c8f4400924b4c79f18dfc4613624d7d54784df07efaff97c1ff2659f3e085ecf4fff493300adc4837553104cef2634110b0d5337
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"lodash.isstring@npm:^4.0.1":
|
||||||
|
version: 4.0.1
|
||||||
|
resolution: "lodash.isstring@npm:4.0.1"
|
||||||
|
checksum: eaac87ae9636848af08021083d796e2eea3d02e80082ab8a9955309569cb3a463ce97fd281d7dc119e402b2e7d8c54a23914b15d2fc7fff56461511dc8937ba0
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"lodash.once@npm:^4.0.0":
|
||||||
|
version: 4.1.1
|
||||||
|
resolution: "lodash.once@npm:4.1.1"
|
||||||
|
checksum: d768fa9f9b4e1dc6453be99b753906f58990e0c45e7b2ca5a3b40a33111e5d17f6edf2f768786e2716af90a8e78f8f91431ab8435f761fef00f9b0c256f6d245
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"lodash@npm:4.x, lodash@npm:^4.17.21, lodash@npm:^4.17.5, lodash@npm:^4.7.0":
|
"lodash@npm:4.x, lodash@npm:^4.17.21, lodash@npm:^4.17.5, lodash@npm:^4.7.0":
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
resolution: "lodash@npm:4.17.21"
|
resolution: "lodash@npm:4.17.21"
|
||||||
@@ -6138,7 +6253,7 @@ resolve@^1.20.0:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"semver@npm:^5.4.1, semver@npm:^5.7.1":
|
"semver@npm:^5.4.1, semver@npm:^5.6.0, semver@npm:^5.7.1":
|
||||||
version: 5.7.1
|
version: 5.7.1
|
||||||
resolution: "semver@npm:5.7.1"
|
resolution: "semver@npm:5.7.1"
|
||||||
bin:
|
bin:
|
||||||
|
|||||||
Reference in New Issue
Block a user