mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-22 01:43:29 +01:00
Fix bug where streaming didnt work due to correct use of Bearer token (#84)
This commit is contained in:
@@ -36,7 +36,7 @@ import morgan from "morgan";
|
|||||||
import { takeWithRepeats } from "./utils";
|
import { takeWithRepeats } from "./utils";
|
||||||
import { parse } from "./burn";
|
import { parse } from "./burn";
|
||||||
import { axiosImageFetcher, ImageFetcher } from "./subsonic";
|
import { axiosImageFetcher, ImageFetcher } from "./subsonic";
|
||||||
import { JWTSmapiLoginTokens, SmapiAuthTokens, SmapiToken } from "./smapi_auth";
|
import { JWTSmapiLoginTokens, SmapiAuthTokens, smapiTokenFromString } from "./smapi_auth";
|
||||||
|
|
||||||
export const BONOB_ACCESS_TOKEN_HEADER = "bat";
|
export const BONOB_ACCESS_TOKEN_HEADER = "bat";
|
||||||
|
|
||||||
@@ -371,7 +371,7 @@ function server(
|
|||||||
logger.info(
|
logger.info(
|
||||||
`${trace} bnb<- ${req.method} ${req.path}?${JSON.stringify(
|
`${trace} bnb<- ${req.method} ${req.path}?${JSON.stringify(
|
||||||
req.query
|
req.query
|
||||||
)}, headers=${JSON.stringify({ ...req.headers, "authorization": "***" })}`
|
)}, headers=${JSON.stringify({ ...req.headers, "authorization": "*****" })}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const authHeader = E.fromNullable("Missing header");
|
const authHeader = E.fromNullable("Missing header");
|
||||||
@@ -384,7 +384,7 @@ function server(
|
|||||||
E.map(match => match[1]!)
|
E.map(match => match[1]!)
|
||||||
)),
|
)),
|
||||||
E.chain(bearerToken => pipe(
|
E.chain(bearerToken => pipe(
|
||||||
smapiAuthTokens.verify(bearerToken as unknown as SmapiToken),
|
smapiAuthTokens.verify(smapiTokenFromString(bearerToken)),
|
||||||
E.mapLeft(_ => "Bearer token failed to verify")
|
E.mapLeft(_ => "Bearer token failed to verify")
|
||||||
)),
|
)),
|
||||||
E.getOrElseW(() => undefined)
|
E.getOrElseW(() => undefined)
|
||||||
|
|||||||
46
src/smapi.ts
46
src/smapi.ts
@@ -459,30 +459,30 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
refreshAuthToken: async (_, _2, soapyHeaders: SoapyHeaders) =>
|
refreshAuthToken: async (_, _2, soapyHeaders: SoapyHeaders) =>
|
||||||
pipe(
|
pipe(
|
||||||
auth(soapyHeaders?.credentials),
|
auth(soapyHeaders?.credentials),
|
||||||
E.map(({ serviceToken }) => smapiAuthTokens.issue(serviceToken)),
|
E.map(({ serviceToken }) => smapiAuthTokens.issue(serviceToken)),
|
||||||
E.map((newToken) => ({
|
E.map((newToken) => ({
|
||||||
authToken: newToken.token,
|
authToken: newToken.token,
|
||||||
privateKey: newToken.key,
|
privateKey: newToken.key,
|
||||||
})),
|
})),
|
||||||
E.orElse((fault) =>
|
E.orElse((fault) =>
|
||||||
pipe(
|
pipe(
|
||||||
fault.toSmapiFault(smapiAuthTokens),
|
fault.toSmapiFault(smapiAuthTokens),
|
||||||
E.fromPredicate(isSmapiRefreshTokenResultFault, (_) => fault),
|
E.fromPredicate(isSmapiRefreshTokenResultFault, (_) => fault),
|
||||||
E.map((it) => it.Fault.detail.refreshAuthTokenResult)
|
E.map((it) => it.Fault.detail.refreshAuthTokenResult)
|
||||||
)
|
)
|
||||||
),
|
|
||||||
E.map((newToken) => ({
|
|
||||||
refreshAuthTokenResult: {
|
|
||||||
authToken: newToken.authToken,
|
|
||||||
privateKey: newToken.privateKey,
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
E.getOrElseW((fault) => {
|
|
||||||
throw fault.toSmapiFault(smapiAuthTokens);
|
|
||||||
})
|
|
||||||
),
|
),
|
||||||
|
E.map((newToken) => ({
|
||||||
|
refreshAuthTokenResult: {
|
||||||
|
authToken: newToken.authToken,
|
||||||
|
privateKey: newToken.privateKey,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
E.getOrElseW((fault) => {
|
||||||
|
throw fault.toSmapiFault(smapiAuthTokens);
|
||||||
|
})
|
||||||
|
),
|
||||||
getMediaURI: async (
|
getMediaURI: async (
|
||||||
{ id }: { id: string },
|
{ id }: { id: string },
|
||||||
_,
|
_,
|
||||||
|
|||||||
@@ -57,17 +57,17 @@ export class InvalidTokenError extends Error implements ToSmapiFault {
|
|||||||
|
|
||||||
export class ExpiredTokenError extends Error implements ToSmapiFault {
|
export class ExpiredTokenError extends Error implements ToSmapiFault {
|
||||||
_tag = "ExpiredTokenError";
|
_tag = "ExpiredTokenError";
|
||||||
authToken: string;
|
serviceToken: string;
|
||||||
expiredAt: number;
|
expiredAt: number;
|
||||||
|
|
||||||
constructor(authToken: string, expiredAt: number) {
|
constructor(serviceToken: string, expiredAt: number) {
|
||||||
super("SMAPI token has expired");
|
super("SMAPI token has expired");
|
||||||
this.authToken = authToken;
|
this.serviceToken = serviceToken;
|
||||||
this.expiredAt = expiredAt;
|
this.expiredAt = expiredAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
toSmapiFault = (smapiAuthTokens: SmapiAuthTokens) => {
|
toSmapiFault = (smapiAuthTokens: SmapiAuthTokens) => {
|
||||||
const newToken = smapiAuthTokens.issue(this.authToken)
|
const newToken = smapiAuthTokens.issue(this.serviceToken)
|
||||||
return {
|
return {
|
||||||
Fault: {
|
Fault: {
|
||||||
faultcode: "Client.TokenRefreshRequired",
|
faultcode: "Client.TokenRefreshRequired",
|
||||||
@@ -142,8 +142,8 @@ export class JWTSmapiLoginTokens implements SmapiAuthTokens {
|
|||||||
return right((jwt.verify(smapiToken.token, this.secret + this.version + smapiToken.key) as any).serviceToken);
|
return right((jwt.verify(smapiToken.token, this.secret + this.version + smapiToken.key) as any).serviceToken);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if(isTokenExpiredError(e)) {
|
if(isTokenExpiredError(e)) {
|
||||||
const x = ((jwt.verify(smapiToken.token, this.secret + this.version + smapiToken.key, { ignoreExpiration: true })) as any).serviceToken;
|
const serviceToken = ((jwt.verify(smapiToken.token, this.secret + this.version + smapiToken.key, { ignoreExpiration: true })) as any).serviceToken;
|
||||||
return left(new ExpiredTokenError(x, e.expiredAt))
|
return left(new ExpiredTokenError(serviceToken, e.expiredAt))
|
||||||
} else if(isError(e))
|
} else if(isError(e))
|
||||||
return left(new InvalidTokenError(e.message));
|
return left(new InvalidTokenError(e.message));
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import i8n, { randomLang } from "../src/i8n";
|
|||||||
import { SONOS_RECOMMENDED_IMAGE_SIZES } from "../src/smapi";
|
import { SONOS_RECOMMENDED_IMAGE_SIZES } from "../src/smapi";
|
||||||
import { Clock, SystemClock } from "../src/clock";
|
import { Clock, SystemClock } from "../src/clock";
|
||||||
import { formatForURL } from "../src/burn";
|
import { formatForURL } from "../src/burn";
|
||||||
import { ExpiredTokenError, SmapiAuthTokens } from "../src/smapi_auth";
|
import { ExpiredTokenError, SmapiAuthTokens, SmapiToken, smapiTokenAsString } from "../src/smapi_auth";
|
||||||
|
|
||||||
describe("rangeFilterFor", () => {
|
describe("rangeFilterFor", () => {
|
||||||
describe("invalid range header string", () => {
|
describe("invalid range header string", () => {
|
||||||
@@ -750,9 +750,9 @@ describe("server", () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const serviceToken = uuid();
|
const serviceToken = `serviceToken-${uuid()}`;
|
||||||
const trackId = uuid();
|
const trackId = `t-${uuid()}`;
|
||||||
const smapiAuthToken = `smapiAuthToken-${uuid()}`;
|
const smapiAuthToken: SmapiToken = { token: `token-${uuid()}`, key: `key-${uuid()}` };
|
||||||
|
|
||||||
const streamContent = (content: string) => ({
|
const streamContent = (content: string) => ({
|
||||||
pipe: (_: Transform) => {
|
pipe: (_: Transform) => {
|
||||||
@@ -777,7 +777,7 @@ describe("server", () => {
|
|||||||
|
|
||||||
describe("when the Bearer token has expired", () => {
|
describe("when the Bearer token has expired", () => {
|
||||||
it("should return a 401", async () => {
|
it("should return a 401", async () => {
|
||||||
smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(smapiAuthToken, 0)))
|
smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(serviceToken, 0)))
|
||||||
|
|
||||||
const res = await request(server).head(
|
const res = await request(server).head(
|
||||||
bonobUrl
|
bonobUrl
|
||||||
@@ -785,7 +785,7 @@ describe("server", () => {
|
|||||||
pathname: `/stream/track/${trackId}`
|
pathname: `/stream/track/${trackId}`
|
||||||
})
|
})
|
||||||
.path(),
|
.path(),
|
||||||
).set('Authorization', `Bearer ${smapiAuthToken}`);
|
).set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`);
|
||||||
|
|
||||||
expect(res.status).toEqual(401);
|
expect(res.status).toEqual(401);
|
||||||
});
|
});
|
||||||
@@ -816,7 +816,7 @@ describe("server", () => {
|
|||||||
bonobUrl
|
bonobUrl
|
||||||
.append({ pathname: `/stream/track/${trackId}`})
|
.append({ pathname: `/stream/track/${trackId}`})
|
||||||
.path()
|
.path()
|
||||||
).set('Authorization', `Bearer ${smapiAuthToken}`);
|
).set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`);
|
||||||
|
|
||||||
expect(res.status).toEqual(trackStream.status);
|
expect(res.status).toEqual(trackStream.status);
|
||||||
expect(res.headers["content-type"]).toEqual(
|
expect(res.headers["content-type"]).toEqual(
|
||||||
@@ -843,7 +843,7 @@ describe("server", () => {
|
|||||||
.append({ pathname: `/stream/track/${trackId}` })
|
.append({ pathname: `/stream/track/${trackId}` })
|
||||||
.path()
|
.path()
|
||||||
)
|
)
|
||||||
.set('Authorization', `Bearer ${smapiAuthToken}`);
|
.set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`);
|
||||||
|
|
||||||
expect(res.status).toEqual(404);
|
expect(res.status).toEqual(404);
|
||||||
expect(res.body).toEqual({});
|
expect(res.body).toEqual({});
|
||||||
@@ -865,14 +865,14 @@ describe("server", () => {
|
|||||||
|
|
||||||
describe("when the Bearer token has expired", () => {
|
describe("when the Bearer token has expired", () => {
|
||||||
it("should return a 401", async () => {
|
it("should return a 401", async () => {
|
||||||
smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(smapiAuthToken, 0)))
|
smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(serviceToken, 0)))
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(
|
.get(
|
||||||
bonobUrl
|
bonobUrl
|
||||||
.append({ pathname: `/stream/track/${trackId}` })
|
.append({ pathname: `/stream/track/${trackId}` })
|
||||||
.path()
|
.path()
|
||||||
).set('Authorization', `Bearer ${smapiAuthToken}`);
|
).set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`);
|
||||||
|
|
||||||
expect(res.status).toEqual(401);
|
expect(res.status).toEqual(401);
|
||||||
});
|
});
|
||||||
@@ -899,7 +899,7 @@ describe("server", () => {
|
|||||||
bonobUrl
|
bonobUrl
|
||||||
.append({ pathname: `/stream/track/${trackId}` })
|
.append({ pathname: `/stream/track/${trackId}` })
|
||||||
.path()
|
.path()
|
||||||
).set('Authorization', `Bearer ${smapiAuthToken}`);
|
).set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`);
|
||||||
|
|
||||||
expect(res.status).toEqual(404);
|
expect(res.status).toEqual(404);
|
||||||
|
|
||||||
@@ -931,7 +931,7 @@ describe("server", () => {
|
|||||||
bonobUrl
|
bonobUrl
|
||||||
.append({ pathname: `/stream/track/${trackId}` })
|
.append({ pathname: `/stream/track/${trackId}` })
|
||||||
.path()
|
.path()
|
||||||
).set('Authorization', `Bearer ${smapiAuthToken}`);
|
).set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`);
|
||||||
|
|
||||||
expect(res.status).toEqual(stream.status);
|
expect(res.status).toEqual(stream.status);
|
||||||
expect(res.headers["content-type"]).toEqual(
|
expect(res.headers["content-type"]).toEqual(
|
||||||
@@ -971,7 +971,7 @@ describe("server", () => {
|
|||||||
bonobUrl
|
bonobUrl
|
||||||
.append({ pathname: `/stream/track/${trackId}` })
|
.append({ pathname: `/stream/track/${trackId}` })
|
||||||
.path()
|
.path()
|
||||||
).set('Authorization', `Bearer ${smapiAuthToken}`);
|
).set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`);
|
||||||
|
|
||||||
expect(res.status).toEqual(stream.status);
|
expect(res.status).toEqual(stream.status);
|
||||||
expect(res.headers["content-type"]).toEqual(
|
expect(res.headers["content-type"]).toEqual(
|
||||||
@@ -1009,7 +1009,7 @@ describe("server", () => {
|
|||||||
bonobUrl
|
bonobUrl
|
||||||
.append({ pathname: `/stream/track/${trackId}` })
|
.append({ pathname: `/stream/track/${trackId}` })
|
||||||
.path()
|
.path()
|
||||||
).set('Authorization', `Bearer ${smapiAuthToken}`);
|
).set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`);
|
||||||
|
|
||||||
expect(res.status).toEqual(stream.status);
|
expect(res.status).toEqual(stream.status);
|
||||||
expect(res.header["content-type"]).toEqual(
|
expect(res.header["content-type"]).toEqual(
|
||||||
@@ -1048,7 +1048,7 @@ describe("server", () => {
|
|||||||
bonobUrl
|
bonobUrl
|
||||||
.append({ pathname: `/stream/track/${trackId}` })
|
.append({ pathname: `/stream/track/${trackId}` })
|
||||||
.path()
|
.path()
|
||||||
).set('Authorization', `Bearer ${smapiAuthToken}`);
|
).set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`);
|
||||||
|
|
||||||
expect(res.status).toEqual(stream.status);
|
expect(res.status).toEqual(stream.status);
|
||||||
expect(res.header["content-type"]).toEqual(
|
expect(res.header["content-type"]).toEqual(
|
||||||
@@ -1093,7 +1093,7 @@ describe("server", () => {
|
|||||||
.append({ pathname: `/stream/track/${trackId}` })
|
.append({ pathname: `/stream/track/${trackId}` })
|
||||||
.path()
|
.path()
|
||||||
)
|
)
|
||||||
.set('Authorization', `Bearer ${smapiAuthToken}`)
|
.set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`)
|
||||||
.set("Range", requestedRange);
|
.set("Range", requestedRange);
|
||||||
|
|
||||||
expect(res.status).toEqual(stream.status);
|
expect(res.status).toEqual(stream.status);
|
||||||
@@ -1137,7 +1137,7 @@ describe("server", () => {
|
|||||||
.append({ pathname: `/stream/track/${trackId}` })
|
.append({ pathname: `/stream/track/${trackId}` })
|
||||||
.path()
|
.path()
|
||||||
)
|
)
|
||||||
.set('Authorization', `Bearer ${smapiAuthToken}`)
|
.set('Authorization', `Bearer ${smapiTokenAsString(smapiAuthToken)}`)
|
||||||
.set("Range", "4000-5000");
|
.set("Range", "4000-5000");
|
||||||
|
|
||||||
expect(res.status).toEqual(stream.status);
|
expect(res.status).toEqual(stream.status);
|
||||||
|
|||||||
Reference in New Issue
Block a user