Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 29 additions & 15 deletions server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ async function baseHandler(req: Request): Promise<Response> {
const { pathname } = new URL(req.url);

if (req.method === "GET" && pathname === "/") {
return new Response("There is no endpoint here, you can read the API spec at https://github.com/kikkia/yt-cipher?tab=readme-ov-file#api-specification. If you are using yt-source/lavalink, use this url for your remote cipher url", {
status: 200,
headers: { "Content-Type": "text/plain" },
});
return new Response(
"There is no endpoint here, you can read the API spec at https://github.com/kikkia/yt-cipher?tab=readme-ov-file#api-specification. If you are using yt-source/lavalink, use this url for your remote cipher url",
{
status: 200,
headers: { "Content-Type": "text/plain" },
},
);
}

if (pathname === '/metrics') {
if (pathname === "/metrics") {
return new Response(registry.metrics(), {
headers: { "Content-Type": "text/plain" },
});
Expand All @@ -30,28 +33,39 @@ async function baseHandler(req: Request): Promise<Response> {
const authHeader = req.headers.get("authorization");
if (API_TOKEN && API_TOKEN !== "") {
if (authHeader !== API_TOKEN) {
const error = authHeader ? 'Invalid API token' : 'Missing API token';
return new Response(JSON.stringify({ error }), { status: 401, headers: { "Content-Type": "application/json" } });
const error = authHeader
? "Invalid API token"
: "Missing API token";
return new Response(JSON.stringify({ error }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
}

let handle: (ctx: RequestContext) => Promise<Response>;

if (pathname === '/decrypt_signature') {
if (pathname === "/decrypt_signature") {
handle = handleDecryptSignature;
} else if (pathname === '/get_sts') {
} else if (pathname === "/get_sts") {
handle = handleGetSts;
} else if (pathname === '/resolve_url') {
} else if (pathname === "/resolve_url") {
handle = handleResolveUrl;
} else {
return new Response(JSON.stringify({ error: 'Not Found' }), { status: 404, headers: { "Content-Type": "application/json" } });
return new Response(JSON.stringify({ error: "Not Found" }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
}

let body;
try {
body = await req.json() as ApiRequest;
} catch {
return new Response(JSON.stringify({ error: 'Invalid JSON body' }), { status: 400, headers: { "Content-Type": "application/json" } });
return new Response(JSON.stringify({ error: "Invalid JSON body" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
const ctx: RequestContext = { req, body };

Expand All @@ -61,11 +75,11 @@ async function baseHandler(req: Request): Promise<Response> {

const handler = baseHandler;

const port = Deno.env.get("PORT") || 8001;
const host = Deno.env.get("HOST") || '0.0.0.0';
const port = Deno.env.get("PORT") || "8001";
const host = Deno.env.get("HOST") || "0.0.0.0";

await initializeCache();
initializeWorkers();

console.log(`Server listening on http://${host}:${port}`);
await serve(handler, { port: Number(port), hostname: host });
await serve(handler, { port: Number(port), hostname: host });
31 changes: 23 additions & 8 deletions src/handlers/decryptSignature.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
import { getSolvers } from "../solver.ts";
import type { RequestContext, SignatureRequest, SignatureResponse } from "../types.ts";
import type {
RequestContext,
SignatureRequest,
SignatureResponse,
} from "../types.ts";

export async function handleDecryptSignature(ctx: RequestContext): Promise<Response> {
const { encrypted_signature, n_param, player_url } = ctx.body as SignatureRequest;
export async function handleDecryptSignature(
ctx: RequestContext,
): Promise<Response> {
const { encrypted_signature, n_param, player_url } = ctx
.body as SignatureRequest;

const solvers = await getSolvers(player_url);

if (!solvers) {
return new Response(JSON.stringify({ error: "Failed to generate solvers from player script" }), { status: 500, headers: { "Content-Type": "application/json" } });
return new Response(
JSON.stringify({
error: "Failed to generate solvers from player script",
}),
{ status: 500, headers: { "Content-Type": "application/json" } },
);
}

let decrypted_signature = '';
let decrypted_signature = "";
if (encrypted_signature && solvers.sig) {
decrypted_signature = solvers.sig(encrypted_signature);
}

let decrypted_n_sig = '';
let decrypted_n_sig = "";
if (n_param && solvers.n) {
decrypted_n_sig = solvers.n(n_param);
}
Expand All @@ -25,5 +37,8 @@ export async function handleDecryptSignature(ctx: RequestContext): Promise<Respo
decrypted_n_sig,
};

return new Response(JSON.stringify(response), { status: 200, headers: { "Content-Type": "application/json" } });
}
return new Response(JSON.stringify(response), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
23 changes: 16 additions & 7 deletions src/handlers/getSts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ export async function handleGetSts(ctx: RequestContext): Promise<Response> {
const response: StsResponse = { sts: cachedSts };
return new Response(JSON.stringify(response), {
status: 200,
headers: { "Content-Type": "application/json", "X-Cache-Hit": "true" },
headers: {
"Content-Type": "application/json",
"X-Cache-Hit": "true",
},
});
}

Expand All @@ -26,12 +29,18 @@ export async function handleGetSts(ctx: RequestContext): Promise<Response> {
const response: StsResponse = { sts };
return new Response(JSON.stringify(response), {
status: 200,
headers: { "Content-Type": "application/json", "X-Cache-Hit": "false" },
headers: {
"Content-Type": "application/json",
"X-Cache-Hit": "false",
},
});
} else {
return new Response(JSON.stringify({ error: "Timestamp not found in player script" }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
return new Response(
JSON.stringify({ error: "Timestamp not found in player script" }),
{
status: 404,
headers: { "Content-Type": "application/json" },
},
);
}
}
}
52 changes: 43 additions & 9 deletions src/handlers/resolveUrl.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,46 @@
import { getSolvers } from "../solver.ts";
import type { RequestContext, ResolveUrlRequest, ResolveUrlResponse } from "../types.ts";
import type {
RequestContext,
ResolveUrlRequest,
ResolveUrlResponse,
} from "../types.ts";

export async function handleResolveUrl(ctx: RequestContext): Promise<Response> {
const { stream_url, player_url, encrypted_signature, signature_key, n_param: nParamFromRequest } = ctx.body as ResolveUrlRequest;
const {
stream_url,
player_url,
encrypted_signature,
signature_key,
n_param: nParamFromRequest,
} = ctx.body as ResolveUrlRequest;

const solvers = await getSolvers(player_url);

if (!solvers) {
return new Response(JSON.stringify({ error: "Failed to generate solvers from player script" }), { status: 500, headers: { "Content-Type": "application/json" } });
return new Response(
JSON.stringify({
error: "Failed to generate solvers from player script",
}),
{ status: 500, headers: { "Content-Type": "application/json" } },
);
}

const url = new URL(stream_url);

if (encrypted_signature) {
if (!solvers.sig) {
return new Response(JSON.stringify({ error: "No signature solver found for this player" }), { status: 500, headers: { "Content-Type": "application/json" } });
return new Response(
JSON.stringify({
error: "No signature solver found for this player",
}),
{
status: 500,
headers: { "Content-Type": "application/json" },
},
);
}
const decryptedSig = solvers.sig(encrypted_signature);
const sigKey = signature_key || 'sig';
const sigKey = signature_key || "sig";
url.searchParams.set(sigKey, decryptedSig);
url.searchParams.delete("s");
}
Expand All @@ -29,15 +52,26 @@ export async function handleResolveUrl(ctx: RequestContext): Promise<Response> {

if (solvers.n) {
if (!nParam) {
return new Response(JSON.stringify({ error: "n_param not found in request or stream_url" }), { status: 400, headers: { "Content-Type": "application/json" } });
return new Response(
JSON.stringify({
error: "n_param not found in request or stream_url",
}),
{
status: 400,
headers: { "Content-Type": "application/json" },
},
);
}
const decryptedN = solvers.n(nParam);
url.searchParams.set("n", decryptedN);
}

const response: ResolveUrlResponse = {
resolved_url: url.toString(),
};

return new Response(JSON.stringify(response), { status: 200, headers: { "Content-Type": "application/json" } });
}
return new Response(JSON.stringify(response), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
2 changes: 1 addition & 1 deletion src/instrumentedCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ export class InstrumentedLRU<T> extends LRU<T> {
super.remove(key);
cacheSize.labels({ cache_name: this.cacheName }).set(this.size);
}
}
}
11 changes: 9 additions & 2 deletions src/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@ export const endpointHits = Counter.with({
export const responseCodes = Counter.with({
name: "http_responses_total",
help: "Total number of HTTP responses.",
labels: ["method", "pathname", "status", "player_id", "plugin_version", "user_agent"],
labels: [
"method",
"pathname",
"status",
"player_id",
"plugin_version",
"user_agent",
],
registry: [registry],
});

Expand Down Expand Up @@ -58,4 +65,4 @@ export const playerScriptFetches = Counter.with({
help: "Total number of player script fetches.",
labels: ["player_url", "status"],
registry: [registry],
});
});
38 changes: 31 additions & 7 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { extractPlayerId } from "./utils.ts";
import { endpointHits, responseCodes, endpointLatency } from "./metrics.ts";
import { endpointHits, endpointLatency, responseCodes } from "./metrics.ts";
import type { RequestContext } from "./types.ts";

type Next = (ctx: RequestContext) => Promise<Response>;
Expand All @@ -8,24 +8,48 @@ export function withMetrics(handler: Next): Next {
return async (ctx: RequestContext) => {
const { pathname } = new URL(ctx.req.url);
const playerId = extractPlayerId(ctx.body.player_url);
const pluginVersion = ctx.req.headers.get("Plugin-Version") ?? "unknown";
const pluginVersion = ctx.req.headers.get("Plugin-Version") ??
"unknown";
const userAgent = ctx.req.headers.get("User-Agent") ?? "unknown";

endpointHits.labels({ method: ctx.req.method, pathname, player_id: playerId, plugin_version: pluginVersion, user_agent: userAgent }).inc();
endpointHits.labels({
method: ctx.req.method,
pathname,
player_id: playerId,
plugin_version: pluginVersion,
user_agent: userAgent,
}).inc();
const start = performance.now();

let response: Response;
try {
response = await handler(ctx);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
response = new Response(JSON.stringify({ error: message }), { status: 500, headers: { "Content-Type": "application/json" } });
response = new Response(JSON.stringify({ error: message }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}

const duration = (performance.now() - start) / 1000;
const cached = response.headers.get("X-Cache-Hit") === "true" ? "true" : "false";
endpointLatency.labels({ method: ctx.req.method, pathname, player_id: playerId, cached}).observe(duration);
responseCodes.labels({ method: ctx.req.method, pathname, status: String(response.status), player_id: playerId, plugin_version: pluginVersion, user_agent: userAgent }).inc();
const cached = response.headers.get("X-Cache-Hit") === "true"
? "true"
: "false";
endpointLatency.labels({
method: ctx.req.method,
pathname,
player_id: playerId,
cached,
}).observe(duration);
responseCodes.labels({
method: ctx.req.method,
pathname,
status: String(response.status),
player_id: playerId,
plugin_version: pluginVersion,
user_agent: userAgent,
}).inc();

return response;
};
Expand Down
Loading