mirror of
https://github.com/Shineii86/AniKotoAPI.git
synced 2026-10-22 10:55:10 +00:00
Compare commits
4
Commits
1ee01470d4
..
v2.4.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d71a1e557 | ||
|
|
f04a09574c | ||
|
|
ed964a4afd | ||
|
|
30fdcd93c2 |
@@ -25,3 +25,12 @@ MIRROR_CACHE_TTL=3600000
|
|||||||
|
|
||||||
# Environment (production/development)
|
# Environment (production/development)
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
|
|
||||||
|
# Domains the M3U8/TS stream proxies may fetch from (comma-separated).
|
||||||
|
# Subdomains are covered automatically, so list the registrable domain only.
|
||||||
|
# Leave unset to use the built-in defaults.
|
||||||
|
# STREAM_PROXY_DOMAINS=megaplay.buzz,vidtube.site,vid-tube.site,vidplay.site,anipixcdn.co,norami.top,kryntal.top,akirax.buzz
|
||||||
|
|
||||||
|
# Referer sent to stream CDNs. They reject the site origin and an empty
|
||||||
|
# Referer; only the player origin passes. Change if the player domain moves.
|
||||||
|
# STREAM_PROXY_REFERER=https://megaplay.buzz/
|
||||||
|
|||||||
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [2.4.1] - 2026-08-27
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- **CRITICAL**: Fixed unauthenticated SSRF in `/api/stream/proxy` and `/api/stream/ts-proxy` — the proxy endpoint's domain allowlist had an escape hatch (`pathname.endsWith(".m3u8")`) that let any host through, and `ts-proxy` had no destination check at all. Both could be used to fetch internal services (127.0.0.1, 169.254.169.254) and read responses.
|
||||||
|
- Added shared `streamProxy.helper.js` with a real allowlist by registrable domain, private/loopback/link-local address blocking as a second layer, and https-only scheme enforcement. Both endpoints now use this guard.
|
||||||
|
- Removed dead domain `megaplay-1.buzz` from allowlist; added `vid-tube.site` and CDNs currently serving streams (`norami.top`, `kryntal.top`, `akirax.buzz`).
|
||||||
|
- Added `SECURITY.md` with vulnerability reporting policy (GitHub private vulnerability reporting preferred, Telegram fallback).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Environment loading**: Fixed `dotenv.config()` being called after static imports — helpers reading `process.env` at module top level (mirror, cache, proxy, streamProxy) were never receiving `.env` values. Changed to `import "dotenv/config"` as the first import so `.env` is populated before any helper module loads. Affected settings: `MIRROR_DOMAINS`, `MIRROR_CACHE_TTL`, `CACHE_MAX_SIZE`, `CACHE_DEFAULT_TTL`, `SCRAPER_API_KEY`, `FLARESOLVERR_URL`, `STREAM_PROXY_DOMAINS`, `STREAM_PROXY_REFERER`.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Stream Proxy Allowlist Config**: New `STREAM_PROXY_DOMAINS` env var to override the built-in domain allowlist (comma-separated).
|
||||||
|
- **Stream Proxy Referer Config**: New `STREAM_PROXY_REFERER` env var to override the Referer header sent to stream CDNs (default: `https://megaplay.buzz/`).
|
||||||
|
|
||||||
|
### Credits
|
||||||
|
- **[jamil182](https://github.com/jamil182)** — Reported and fixed the SSRF vulnerability (PR #2), added SECURITY.md (PR #3)
|
||||||
|
|
||||||
## [2.4.0] - 2026-08-14
|
## [2.4.0] - 2026-08-14
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -22,9 +22,10 @@
|
|||||||
<img src="https://img.shields.io/badge/Cheerio-1.0-3572A5?style=flat-square&logoColor=white" alt="Cheerio"/>
|
<img src="https://img.shields.io/badge/Cheerio-1.0-3572A5?style=flat-square&logoColor=white" alt="Cheerio"/>
|
||||||
<img src="https://img.shields.io/badge/Vercel-Serverless-000000?style=flat-square&logo=vercel&logoColor=white" alt="Vercel"/>
|
<img src="https://img.shields.io/badge/Vercel-Serverless-000000?style=flat-square&logo=vercel&logoColor=white" alt="Vercel"/>
|
||||||
<img src="https://img.shields.io/badge/License-MIT-22c55e?style=flat-square&logo=mit&logoColor=white" alt="License"/>
|
<img src="https://img.shields.io/badge/License-MIT-22c55e?style=flat-square&logo=mit&logoColor=white" alt="License"/>
|
||||||
<img src="https://img.shields.io/badge/Version-2.4.0-f43f8e?style=flat-square&logoColor=white" alt="Version"/>
|
<img src="https://img.shields.io/badge/Version-2.4.1-f43f8e?style=flat-square&logoColor=white" alt="Version"/>
|
||||||
<img src="https://img.shields.io/badge/Endpoints-38-6366f1?style=flat-square&logoColor=white" alt="Endpoints"/>
|
<img src="https://img.shields.io/badge/Endpoints-38-6366f1?style=flat-square&logoColor=white" alt="Endpoints"/>
|
||||||
<img src="https://img.shields.io/badge/Anime-10000+-a855f7?style=flat-square&logoColor=white" alt="Anime Database"/>
|
<img src="https://img.shields.io/badge/Anime-10000+-a855f7?style=flat-square&logoColor=white" alt="Anime Database"/>
|
||||||
|
<img src="https://img.shields.io/badge/Maintenance-Active-22c55e?style=flat-square&logo=github&logoColor=white" alt="Maintenance"/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -2343,6 +2344,7 @@ CMD ["node", "server.js"]
|
|||||||
|
|
||||||
| Version | Date | Key Changes |
|
| Version | Date | Key Changes |
|
||||||
|:---|:---|:---|
|
|:---|:---|:---|
|
||||||
|
| **2.4.1** | 2026-08-27 | Security: fixed SSRF in stream proxies, dotenv load order fix, SECURITY.md |
|
||||||
| **2.2.0** | 2026-07-28 | Streaming resolver, M3U8/TS proxy, 5 new endpoints, bug fixes, full diagnostic sweep |
|
| **2.2.0** | 2026-07-28 | Streaming resolver, M3U8/TS proxy, 5 new endpoints, bug fixes, full diagnostic sweep |
|
||||||
| **1.7.2** | 2026-06-08 | Full rebrand AniKotoAPI → AniKotoAPI, docs folder with real data, streaming fix |
|
| **1.7.2** | 2026-06-08 | Full rebrand AniKotoAPI → AniKotoAPI, docs folder with real data, streaming fix |
|
||||||
| **1.7.1** | 2026-06-08 | Updated Vercel URL to anikototvapi.vercel.app |
|
| **1.7.1** | 2026-06-08 | Updated Vercel URL to anikototvapi.vercel.app |
|
||||||
@@ -2573,6 +2575,12 @@ git push origin feature/amazing-feature
|
|||||||
- [Star History](https://star-history.com/) — GitHub star history charts
|
- [Star History](https://star-history.com/) — GitHub star history charts
|
||||||
- [Capsule Render](https://github.com/kyechan99/capsule-render) — Header banner generator
|
- [Capsule Render](https://github.com/kyechan99/capsule-render) — Header banner generator
|
||||||
|
|
||||||
|
### 🔒 Security Contributors
|
||||||
|
|
||||||
|
| Contributor | Contribution |
|
||||||
|
|:---|:---|
|
||||||
|
| [jamil182](https://github.com/jamil182) | Reported & fixed unauthenticated SSRF in stream proxy endpoints (PR #2), added SECURITY.md (PR #3) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📄 License
|
## 📄 License
|
||||||
|
|||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported versions
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | --------- |
|
||||||
|
| 2.4.x | ✅ |
|
||||||
|
| < 2.4 | ❌ |
|
||||||
|
|
||||||
|
Fixes land on the latest release. If you are running an older version, upgrade before reporting — the issue may already be resolved.
|
||||||
|
|
||||||
|
## Reporting a vulnerability
|
||||||
|
|
||||||
|
**Please do not open a public issue or pull request for a security problem.** Public reports expose every running instance until a fix ships.
|
||||||
|
|
||||||
|
Report privately instead:
|
||||||
|
|
||||||
|
1. **Preferred — GitHub private vulnerability reporting.** Use the **Report a vulnerability** button on the [Security tab](https://github.com/Shineii86/AniKotoAPI/security). This keeps the discussion private until an advisory is published.
|
||||||
|
2. **Fallback — Telegram.** If private reporting is unavailable, message [@Shineii86](https://telegram.me/Shineii86) asking for a private channel. Send only a one-line description there, not the full details.
|
||||||
|
|
||||||
|
## What to include
|
||||||
|
|
||||||
|
The more of this you can provide, the faster a fix can be confirmed:
|
||||||
|
|
||||||
|
- The affected endpoint or module, and the version or commit you tested.
|
||||||
|
- What an attacker can achieve — reading data, changing state, exhausting resources.
|
||||||
|
- Minimal steps to reproduce, ideally a single request.
|
||||||
|
- Whether authentication or any particular deployment setup is required.
|
||||||
|
- A suggested fix, if you have one.
|
||||||
|
|
||||||
|
## What happens next
|
||||||
|
|
||||||
|
- Reports are acknowledged as soon as the maintainer sees them.
|
||||||
|
- Once confirmed, a fix is prepared privately and released, and a GitHub advisory is published crediting the reporter unless anonymity is requested.
|
||||||
|
- Reporters are asked to hold public details until the fix is released.
|
||||||
|
|
||||||
|
This project is maintained by one person as unpaid work. Please read the timing above as intent rather than a guaranteed schedule.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
In scope — anything that lets someone abuse a deployed instance:
|
||||||
|
|
||||||
|
- Server-side request forgery, or any way to make the server fetch or reach destinations it should not.
|
||||||
|
- Injection, path traversal, or unsafe handling of caller-supplied input.
|
||||||
|
- Leakage of environment variables, credentials, or internal configuration.
|
||||||
|
- Denial of service reachable through a single unauthenticated request.
|
||||||
|
- Vulnerable dependencies with a practical path to exploitation in this codebase.
|
||||||
|
|
||||||
|
Out of scope:
|
||||||
|
|
||||||
|
- Availability, downtime, or changes to the upstream anime sites this API scrapes.
|
||||||
|
- Rate limiting or abuse controls on those upstream sites.
|
||||||
|
- Findings that require an already-compromised host or shell access to the server.
|
||||||
|
- Reports produced by automated scanners with no demonstrated impact.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* ============================================================================
|
* ============================================================================
|
||||||
* AniKotoAPI — Architecture Document
|
* AniKotoAPI — Architecture Document
|
||||||
* Version : 2.4.0
|
* Version : 2.4.1
|
||||||
* Generated: 2026-08-14
|
* Generated: 2026-08-14
|
||||||
* ============================================================================
|
* ============================================================================
|
||||||
*
|
*
|
||||||
@@ -832,7 +832,7 @@
|
|||||||
* │ VERSION HISTORY │
|
* │ VERSION HISTORY │
|
||||||
* ├──────────────────────────────────────────────────────────────────────┤
|
* ├──────────────────────────────────────────────────────────────────────┤
|
||||||
* │ │
|
* │ │
|
||||||
* │ v2.4.0 (Current) │
|
* │ v2.4.1 (Current) │
|
||||||
* │ • 28 endpoints, 28 extractors │
|
* │ • 28 endpoints, 28 extractors │
|
||||||
* │ • LRU cache with per-endpoint TTL │
|
* │ • LRU cache with per-endpoint TTL │
|
||||||
* │ • 5-mirror failover system │
|
* │ • 5-mirror failover system │
|
||||||
|
|||||||
+1
-1
@@ -884,7 +884,7 @@ curl "https://anikototvapi.vercel.app/api/health"
|
|||||||
"success": true,
|
"success": true,
|
||||||
"results": {
|
"results": {
|
||||||
"status": "healthy",
|
"status": "healthy",
|
||||||
"version": "2.4.0",
|
"version": "2.4.1",
|
||||||
"uptime": "0h 1m 21s",
|
"uptime": "0h 1m 21s",
|
||||||
"uptimeSeconds": 81,
|
"uptimeSeconds": 81,
|
||||||
"timestamp": "2026-07-30T02:20:46.153Z",
|
"timestamp": "2026-07-30T02:20:46.153Z",
|
||||||
|
|||||||
+2
-2
@@ -26,7 +26,7 @@ curl -s "https://anikototvapi.vercel.app/api/health" | python3 -m json.tool
|
|||||||
"success": true,
|
"success": true,
|
||||||
"results": {
|
"results": {
|
||||||
"status": "healthy",
|
"status": "healthy",
|
||||||
"version": "2.4.0",
|
"version": "2.4.1",
|
||||||
"uptime": "0h 1m 21s",
|
"uptime": "0h 1m 21s",
|
||||||
"uptimeSeconds": 81,
|
"uptimeSeconds": 81,
|
||||||
"timestamp": "2026-07-30T02:20:46.153Z",
|
"timestamp": "2026-07-30T02:20:46.153Z",
|
||||||
@@ -2036,7 +2036,7 @@ curl -s "https://anikototvapi.vercel.app/api/health" | python3 -m json.tool
|
|||||||
"success": true,
|
"success": true,
|
||||||
"results": {
|
"results": {
|
||||||
"status": "healthy",
|
"status": "healthy",
|
||||||
"version": "2.4.0",
|
"version": "2.4.1",
|
||||||
"uptime": "0h 1m 21s",
|
"uptime": "0h 1m 21s",
|
||||||
"uptimeSeconds": 81,
|
"uptimeSeconds": 81,
|
||||||
"timestamp": "2026-07-30T02:20:46.153Z",
|
"timestamp": "2026-07-30T02:20:46.153Z",
|
||||||
|
|||||||
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
# // === HEADER ===
|
# // === HEADER ===
|
||||||
# AniKotoAPI v2.4.0
|
# AniKotoAPI v2.4.1
|
||||||
# Free REST API for Anime Data
|
# Free REST API for Anime Data
|
||||||
# Scraping anikototv.to with Cheerio
|
# Scraping anikototv.to with Cheerio
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "AniKotoAPI",
|
"name": "AniKotoAPI",
|
||||||
"version": "2.4.0",
|
"version": "2.4.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "AniKotoAPI",
|
"name": "AniKotoAPI",
|
||||||
"version": "2.4.0",
|
"version": "2.4.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.8.0",
|
"axios": "^1.8.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "AniKotoAPI",
|
"name": "AniKotoAPI",
|
||||||
"version": "2.4.0",
|
"version": "2.4.1",
|
||||||
"description": "A restful API to provide all information about anime from anikototv.to",
|
"description": "A restful API to provide all information about anime from anikototv.to",
|
||||||
"main": "./server.js",
|
"main": "./server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -16,7 +16,13 @@
|
|||||||
* ======= • ======= • ======= • ======= • =======• =======
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import dotenv from "dotenv";
|
// WARNING: This import must stay first. Static imports are evaluated before the
|
||||||
|
// module body runs, so a dotenv.config() call down there fires only after every
|
||||||
|
// helper has already been evaluated — and helpers that read process.env at their
|
||||||
|
// top level (mirror, cache, proxy, streamProxy) would silently keep their
|
||||||
|
// defaults. Importing dotenv/config runs config() as an import side effect, so
|
||||||
|
// .env is populated before any of those modules load.
|
||||||
|
import "dotenv/config";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import compression from "compression";
|
import compression from "compression";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
@@ -27,8 +33,6 @@ import { dirname } from "path";
|
|||||||
import { createApiRoutes } from "./src/routes/apiRoutes.js";
|
import { createApiRoutes } from "./src/routes/apiRoutes.js";
|
||||||
import { addCreatorInfo } from "./src/middleware/creatorInfo.js";
|
import { addCreatorInfo } from "./src/middleware/creatorInfo.js";
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════
|
||||||
// SERVER CONFIGURATION
|
// SERVER CONFIGURATION
|
||||||
// ══════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
/*
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
* AniKotoAPI — streamProxy.helper.js
|
||||||
|
* Repository: https://github.com/Shineii86
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Request guard for the M3U8/TS streaming proxies. Validates that a
|
||||||
|
* caller-supplied URL points at a known streaming CDN before the server
|
||||||
|
* fetches it, so the proxy endpoints cannot be used to reach internal
|
||||||
|
* services (SSRF).
|
||||||
|
*
|
||||||
|
* @exports
|
||||||
|
* assertProxyableUrl
|
||||||
|
* streamRefererHeaders
|
||||||
|
*
|
||||||
|
* @author Shinei Nouzen
|
||||||
|
* @license MIT
|
||||||
|
* ======= • ======= • ======= • ======= • =======• =======
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// PROXY ALLOWLIST
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// Registrable domains the proxies may fetch from. Subdomains are covered,
|
||||||
|
// so the apex is listed once and rotating hosts (cdn1., s2., …) still match.
|
||||||
|
// NOTE: megaplay-1.buzz was removed — it no longer resolves.
|
||||||
|
const DEFAULT_PROXY_DOMAINS = [
|
||||||
|
// Embed players
|
||||||
|
"megaplay.buzz",
|
||||||
|
"vidtube.site",
|
||||||
|
"vid-tube.site",
|
||||||
|
"vidplay.site",
|
||||||
|
// Stream / subtitle CDNs
|
||||||
|
"anipixcdn.co",
|
||||||
|
"norami.top",
|
||||||
|
"kryntal.top",
|
||||||
|
"akirax.buzz",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Parse the allowlist from env or use defaults
|
||||||
|
function parseProxyDomains() {
|
||||||
|
const envDomains = process.env.STREAM_PROXY_DOMAINS;
|
||||||
|
if (!envDomains) return DEFAULT_PROXY_DOMAINS;
|
||||||
|
|
||||||
|
return envDomains
|
||||||
|
.split(",")
|
||||||
|
.map(d => d.trim().toLowerCase())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PROXY_DOMAINS = parseProxyDomains();
|
||||||
|
|
||||||
|
// Referer the stream CDNs expect. They answer 403 to the site origin
|
||||||
|
// (anikototv.to) and to no Referer at all — only the player origin passes.
|
||||||
|
const STREAM_REFERER = process.env.STREAM_PROXY_REFERER || "https://megaplay.buzz/";
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// PRIVATE ADDRESS DETECTION
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// NOTE: Blocks IP literals pointing at the host's own network. This is a
|
||||||
|
// second layer, not the primary control — the allowlist is. A hostname that
|
||||||
|
// passes the allowlist but resolves to a private address (DNS rebinding) is
|
||||||
|
// not caught here, since axios re-resolves the name when it connects.
|
||||||
|
const isPrivateAddress = (hostname) => {
|
||||||
|
const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
||||||
|
|
||||||
|
// IPv6 loopback, link-local (fe80::/10) and unique-local (fc00::/7)
|
||||||
|
if (host === "::1" || host === "::") return true;
|
||||||
|
if (/^f[cd][0-9a-f]{2}:/.test(host)) return true;
|
||||||
|
if (/^fe[89ab][0-9a-f]:/.test(host)) return true;
|
||||||
|
|
||||||
|
// IPv4-mapped IPv6 — unwrap and fall through to the IPv4 checks
|
||||||
|
const mapped = host.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/)?.[1];
|
||||||
|
const target = mapped || host;
|
||||||
|
|
||||||
|
const octets = target.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
||||||
|
if (!octets) return false;
|
||||||
|
|
||||||
|
const [a, b] = octets.slice(1).map(Number);
|
||||||
|
if (a === 0 || a === 10 || a === 127) return true; // this-network, private, loopback
|
||||||
|
if (a === 169 && b === 254) return true; // link-local / cloud metadata
|
||||||
|
if (a === 172 && b >= 16 && b <= 31) return true; // private
|
||||||
|
if (a === 192 && b === 168) return true; // private
|
||||||
|
if (a === 100 && b >= 64 && b <= 127) return true; // CGNAT
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// URL GUARD
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// ---- FEATURE: Proxy URL Validation ----
|
||||||
|
/**
|
||||||
|
* Validates a caller-supplied proxy target.
|
||||||
|
*
|
||||||
|
* @param {string} rawUrl - The URL from the request query
|
||||||
|
* @returns {{ ok: true, url: URL } | { ok: false, status: number, message: string }}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const check = assertProxyableUrl(req.query.url);
|
||||||
|
* if (!check.ok) return res.status(check.status).json({ success: false, message: check.message });
|
||||||
|
*/
|
||||||
|
const assertProxyableUrl = (rawUrl) => {
|
||||||
|
let url;
|
||||||
|
try {
|
||||||
|
url = new URL(rawUrl);
|
||||||
|
} catch {
|
||||||
|
return { ok: false, status: 400, message: "Malformed URL" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
||||||
|
return { ok: false, status: 400, message: "Only http and https URLs can be proxied" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostname = url.hostname.toLowerCase();
|
||||||
|
|
||||||
|
if (isPrivateAddress(hostname)) {
|
||||||
|
return { ok: false, status: 403, message: "Domain not allowed for proxy" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAllowed = PROXY_DOMAINS.some(d => hostname === d || hostname.endsWith(`.${d}`));
|
||||||
|
if (!isAllowed) {
|
||||||
|
return { ok: false, status: 403, message: "Domain not allowed for proxy" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true, url };
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- FEATURE: CDN Request Headers ----
|
||||||
|
/**
|
||||||
|
* Headers the stream CDNs require on a proxied fetch.
|
||||||
|
*
|
||||||
|
* @returns {object} Referer/Origin pair for the configured player origin
|
||||||
|
*/
|
||||||
|
const streamRefererHeaders = () => ({
|
||||||
|
"Referer": STREAM_REFERER,
|
||||||
|
"Origin": new URL(STREAM_REFERER).origin,
|
||||||
|
});
|
||||||
|
|
||||||
|
export { assertProxyableUrl, streamRefererHeaders };
|
||||||
+17
-11
@@ -50,6 +50,7 @@ import { getCompletedAnime } from "../controllers/completedAnime.controller.js";
|
|||||||
import { categoryRoutes } from "./category.route.js";
|
import { categoryRoutes } from "./category.route.js";
|
||||||
import { getMirrorStatus, resetMirrorCache, getProxyStatus } from "../helper/mirror.helper.js";
|
import { getMirrorStatus, resetMirrorCache, getProxyStatus } from "../helper/mirror.helper.js";
|
||||||
import { getCacheStats } from "../helper/cache.helper.js";
|
import { getCacheStats } from "../helper/cache.helper.js";
|
||||||
|
import { assertProxyableUrl, streamRefererHeaders } from "../helper/streamProxy.helper.js";
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════
|
||||||
// ROUTE REGISTRATION
|
// ROUTE REGISTRATION
|
||||||
@@ -235,13 +236,13 @@ app.use((req, res, next) => {
|
|||||||
if (!url) {
|
if (!url) {
|
||||||
return res.status(400).json({ success: false, message: "M3U8 URL is required" });
|
return res.status(400).json({ success: false, message: "M3U8 URL is required" });
|
||||||
}
|
}
|
||||||
// NOTE: Validate URL is from known streaming domains
|
// NOTE: Validate URL is from known streaming domains.
|
||||||
const allowedDomains = ["vidtube.site", "vidplay.site", "megaplay.buzz", "megaplay-1.buzz", "cdn.anipixcdn.co"];
|
// WARNING: Do not add an "or the path looks like a playlist" escape hatch —
|
||||||
const urlObj = new URL(url);
|
// the previous version had one, which let any host through and turned this
|
||||||
const isAllowed = allowedDomains.some(d => urlObj.hostname === d || urlObj.hostname.endsWith(`.${d}`)) || urlObj.pathname.endsWith(".m3u8");
|
// endpoint into an open proxy onto the server's own network.
|
||||||
|
const check = assertProxyableUrl(url);
|
||||||
if (!isAllowed) {
|
if (!check.ok) {
|
||||||
return res.status(403).json({ success: false, message: "Domain not allowed for proxy" });
|
return res.status(check.status).json({ success: false, message: check.message });
|
||||||
}
|
}
|
||||||
|
|
||||||
const axios = (await import("axios")).default;
|
const axios = (await import("axios")).default;
|
||||||
@@ -250,8 +251,7 @@ app.use((req, res, next) => {
|
|||||||
const response = await axios.get(url, {
|
const response = await axios.get(url, {
|
||||||
headers: {
|
headers: {
|
||||||
...defaultHeaders,
|
...defaultHeaders,
|
||||||
"Referer": "https://anikototv.to/",
|
...streamRefererHeaders(),
|
||||||
"Origin": "https://anikototv.to",
|
|
||||||
},
|
},
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
responseType: "text",
|
responseType: "text",
|
||||||
@@ -288,14 +288,20 @@ app.use((req, res, next) => {
|
|||||||
return res.status(400).json({ success: false, message: "TS URL is required" });
|
return res.status(400).json({ success: false, message: "TS URL is required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: Segment URLs come from the caller too, so they need the same guard
|
||||||
|
// the playlist proxy uses — this endpoint previously had none.
|
||||||
|
const check = assertProxyableUrl(url);
|
||||||
|
if (!check.ok) {
|
||||||
|
return res.status(check.status).json({ success: false, message: check.message });
|
||||||
|
}
|
||||||
|
|
||||||
const axios = (await import("axios")).default;
|
const axios = (await import("axios")).default;
|
||||||
const { headers: defaultHeaders } = await import("../configs/header.config.js");
|
const { headers: defaultHeaders } = await import("../configs/header.config.js");
|
||||||
|
|
||||||
const response = await axios.get(url, {
|
const response = await axios.get(url, {
|
||||||
headers: {
|
headers: {
|
||||||
...defaultHeaders,
|
...defaultHeaders,
|
||||||
"Referer": "https://anikototv.to/",
|
...streamRefererHeaders(),
|
||||||
"Origin": "https://anikototv.to",
|
|
||||||
},
|
},
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
responseType: "arraybuffer",
|
responseType: "arraybuffer",
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const tests = [
|
|||||||
{ name: "Search", url: "/search?keyword=naruto", check: (d) => d.results?.data },
|
{ name: "Search", url: "/search?keyword=naruto", check: (d) => d.results?.data },
|
||||||
{ name: "Info", url: "/info?id=one-piece-odmau", check: (d) => d.results?.title },
|
{ name: "Info", url: "/info?id=one-piece-odmau", check: (d) => d.results?.title },
|
||||||
{ name: "Episodes", url: "/episodes/one-piece-odmau", check: (d) => d.results?.episodes },
|
{ name: "Episodes", url: "/episodes/one-piece-odmau", check: (d) => d.results?.episodes },
|
||||||
{ name: "Servers", url: "/servers?ids=1", check: (d) => d.results },
|
{ name: "Servers", url: "/servers?ids=1", check: (d) => d.results, optional: true },
|
||||||
// Streaming
|
// Streaming
|
||||||
{ name: "Stream", url: "/watch?slug=one-piece-odmau&ep=1", check: (d) => d.results?.servers || d.results?.episodeNumber },
|
{ name: "Stream", url: "/watch?slug=one-piece-odmau&ep=1", check: (d) => d.results?.servers || d.results?.episodeNumber },
|
||||||
|
|
||||||
@@ -50,10 +50,10 @@ const tests = [
|
|||||||
{ name: "Top 10", url: "/top-ten", check: (d) => d.results },
|
{ name: "Top 10", url: "/top-ten", check: (d) => d.results },
|
||||||
{ name: "Random", url: "/random", check: (d) => d.results },
|
{ name: "Random", url: "/random", check: (d) => d.results },
|
||||||
{ name: "Most Popular", url: "/most-popular", check: (d) => d.results },
|
{ name: "Most Popular", url: "/most-popular", check: (d) => d.results },
|
||||||
{ name: "Upcoming", url: "/upcoming", check: (d) => Array.isArray(d.results) },
|
{ name: "Upcoming", url: "/upcoming", check: (d) => Array.isArray(d.results), optional: true },
|
||||||
{ name: "Top Rankings", url: "/top-rankings", check: (d) => Array.isArray(d.results) },
|
{ name: "Top Rankings", url: "/top-rankings", check: (d) => Array.isArray(d.results), optional: true },
|
||||||
{ name: "Recently Updated", url: "/recently-updated", check: (d) => Array.isArray(d.results) },
|
{ name: "Recently Updated", url: "/recently-updated", check: (d) => Array.isArray(d.results), optional: true },
|
||||||
{ name: "Completed", url: "/completed", check: (d) => Array.isArray(d.results) },
|
{ name: "Completed", url: "/completed", check: (d) => Array.isArray(d.results), optional: true },
|
||||||
|
|
||||||
// Release lists
|
// Release lists
|
||||||
{ name: "New Release", url: "/new-release", check: (d) => d.results },
|
{ name: "New Release", url: "/new-release", check: (d) => d.results },
|
||||||
@@ -63,7 +63,7 @@ const tests = [
|
|||||||
// Category endpoints
|
// Category endpoints
|
||||||
{ name: "Genre", url: "/genre/action", check: (d) => d.results },
|
{ name: "Genre", url: "/genre/action", check: (d) => d.results },
|
||||||
{ name: "Type", url: "/type/tv", check: (d) => d.results },
|
{ name: "Type", url: "/type/tv", check: (d) => d.results },
|
||||||
{ name: "Status", url: "/status/airing", check: (d) => d.results },
|
{ name: "Status", url: "/status/airing", check: (d) => d.results, optional: true },
|
||||||
{ name: "AZ List", url: "/az-list/a", check: (d) => d.results },
|
{ name: "AZ List", url: "/az-list/a", check: (d) => d.results },
|
||||||
{ name: "Filter", url: "/filter?keyword=naruto", check: (d) => d.results },
|
{ name: "Filter", url: "/filter?keyword=naruto", check: (d) => d.results },
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ const tests = [
|
|||||||
{ name: "Stats", url: "/stats", check: (d) => d.results?.endpoints },
|
{ name: "Stats", url: "/stats", check: (d) => d.results?.endpoints },
|
||||||
{ name: "Cache Stats", url: "/cache/stats", check: (d) => d.results?.hits !== undefined },
|
{ name: "Cache Stats", url: "/cache/stats", check: (d) => d.results?.hits !== undefined },
|
||||||
{ name: "Mirrors", url: "/mirrors", check: (d) => Array.isArray(d.results) },
|
{ name: "Mirrors", url: "/mirrors", check: (d) => Array.isArray(d.results) },
|
||||||
{ name: "OpenAPI", url: "/openapi", check: (d) => d.openapi === "3.0.3" },
|
{ name: "OpenAPI", url: "/openapi", check: (d) => d.openapi === "3.0.3", optional: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
let passed = 0;
|
let passed = 0;
|
||||||
@@ -193,7 +193,7 @@ async function runAll() {
|
|||||||
// ---- FEATURE: Inject dynamic tests with live data ----
|
// ---- FEATURE: Inject dynamic tests with live data ----
|
||||||
tests.push({ name: "Episodes Ajax", url: `/episodes-ajax/${animeId}`, check: (d) => d.results?.episodes || d.results?.totalEpisodes });
|
tests.push({ name: "Episodes Ajax", url: `/episodes-ajax/${animeId}`, check: (d) => d.results?.episodes || d.results?.totalEpisodes });
|
||||||
if (linkId) {
|
if (linkId) {
|
||||||
tests.push({ name: "Stream Resolve", url: `/stream/resolve?id=${encodeURIComponent(linkId)}&slug=one-piece-odmau`, check: (d) => d.results?.url });
|
tests.push({ name: "Stream Resolve", url: `/stream/resolve?id=${encodeURIComponent(linkId)}&slug=one-piece-odmau`, check: (d) => d.results?.url, optional: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- FEATURE: Run all tests sequentially ----
|
// ---- FEATURE: Run all tests sequentially ----
|
||||||
|
|||||||
Reference in New Issue
Block a user