aboutsummaryrefslogtreecommitdiffstats
path: root/src/server
diff options
context:
space:
mode:
Diffstat (limited to 'src/server')
-rw-r--r--src/server/HttpServer.hx24
-rw-r--r--src/server/Main.hx18
-rw-r--r--src/server/YoutubeFallback.hx144
3 files changed, 173 insertions, 13 deletions
diff --git a/src/server/HttpServer.hx b/src/server/HttpServer.hx
index c953bb0..1a0c4d5 100644
--- a/src/server/HttpServer.hx
+++ b/src/server/HttpServer.hx
@@ -187,9 +187,9 @@ class HttpServer {
static function proxyUrl(req:IncomingMessage, res:ServerResponse):Bool {
final url = req.url.replace("/proxy?url=", "");
- final proxy = proxyRequest(url, req, res, proxyReq -> {
- final url = proxyReq.headers["location"] ?? return false;
- final proxy2 = proxyRequest(url, req, res, proxyReq -> false);
+ final proxy = proxyRequest(url, req, res, proxyRes -> {
+ final url = proxyRes.headers["location"] ?? return false;
+ final proxy2 = proxyRequest(url, req, res, proxyRes -> false);
if (proxy2 == null) {
res.end('Proxy error: multiple redirects for url $url');
return true;
@@ -203,8 +203,10 @@ class HttpServer {
}
static function proxyRequest(
- url:String, req:IncomingMessage, res:ServerResponse,
- fn:(req:IncomingMessage) -> Bool
+ url:String,
+ req:IncomingMessage,
+ res:ServerResponse,
+ cancelProxyRequest:(proxyRes:IncomingMessage) -> Bool
):Null<ClientRequest> {
final url = try {
new URL(safeDecodeURI(url));
@@ -216,12 +218,14 @@ class HttpServer {
path: url.pathname + url.search,
method: req.method
};
+ req.headers["referer"] = url.toString();
+ req.headers["host"] = url.hostname;
final request = url.protocol == "https:" ? Https.request : Http.request;
- final proxy = request(options, proxyReq -> {
- if (fn(proxyReq)) return;
- proxyReq.headers["Content-Type"] = "application/octet-stream";
- res.writeHead(proxyReq.statusCode, proxyReq.headers);
- proxyReq.pipe(res);
+ final proxy = request(options, proxyRes -> {
+ if (cancelProxyRequest(proxyRes)) return;
+ proxyRes.headers["Content-Type"] = "application/octet-stream";
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
+ proxyRes.pipe(res);
});
proxy.on("error", err -> {
res.end('Proxy error: ${url.href}');
diff --git a/src/server/Main.hx b/src/server/Main.hx
index c5eb36f..e7c38a6 100644
--- a/src/server/Main.hx
+++ b/src/server/Main.hx
@@ -531,8 +531,7 @@ class Main {
if (userList.admins.exists(
a -> a.name.toLowerCase() == lcName && a.hash == hash)) {
client.isAdmin = true;
- }
- else {
+ } else {
serverMessage(client, "passwordMatchError");
send(client, {type: LoginError});
return;
@@ -587,6 +586,17 @@ class Main {
broadcast(data);
case ServerMessage:
+ case GetYoutubeVideoInfo:
+ final url = data.getYoutubeVideoInfo.url;
+ YoutubeFallback.getInfo(url, info -> {
+ send(client, {
+ type: data.type,
+ getYoutubeVideoInfo: {
+ url: url,
+ response: info
+ }
+ });
+ });
case AddVideo:
if (isPlaylistLockedFor(client)) return;
if (!checkPermission(client, AddVideoPerm)) return;
@@ -1038,7 +1048,9 @@ class Main {
duration: duration,
time: time
});
- while (flashbacks.length > FLASHBACKS_COUNT) flashbacks.pop();
+ while (flashbacks.length > FLASHBACKS_COUNT) {
+ flashbacks.pop();
+ }
}
function isPlaylistLockedFor(client:Client):Bool {
diff --git a/src/server/YoutubeFallback.hx b/src/server/YoutubeFallback.hx
new file mode 100644
index 0000000..34ea8a9
--- /dev/null
+++ b/src/server/YoutubeFallback.hx
@@ -0,0 +1,144 @@
+package server;
+
+import haxe.Json;
+import js.lib.Function;
+import js.lib.Object;
+import js.lib.Promise;
+import js.node.Https.Https;
+import js.node.Https.HttpsRequestOptions;
+import js.node.url.URLSearchParams;
+import utils.YoutubeUtils;
+
+using Lambda;
+
+class YoutubeFallback {
+ static function httpsGet(
+ url:String,
+ ?options:HttpsRequestOptions,
+ ?callback:(status:Int, data:String) -> Void
+ ):Void {
+ final request = Https.get(url, options, res -> {
+ var data = "";
+ res.on("data", chunk -> data += chunk.toString());
+ res.on("end", () -> callback(res.statusCode, data));
+ });
+ request.on("error", err -> {
+ trace(url);
+ trace("request error: ", err);
+ });
+ }
+
+ public static function resolvePlayerResponse(watchHtml:String):String {
+ if (watchHtml == null) return "";
+ final resReg = ~/ytInitialPlayerResponse = (.*)}}};/;
+ var matches = resReg.match(watchHtml);
+ return matches ? resReg.matched(1) + "}}}" : "";
+ }
+
+ public static function resoleM3U8Link(watchHtml:String):Null<String> {
+ if (watchHtml == null) return null;
+ final hlsReg = ~/hlsManifestUrl":"(.*\/file\/index\.m3u8)/;
+ return hlsReg.match(watchHtml) ? hlsReg.matched(1) : null;
+ }
+
+ public static function buildDecoder(watchHtml:String, callback:(decoder:(cipher:String) -> String) -> Void):Void {
+ if (watchHtml == null) return callback(null);
+
+ final jsFileUrlReg = ~/\/s\/player\/[A-Za-z0-9]+\/[A-Za-z0-9_.]+\/[A-Za-z0-9_]+\/base\.js/;
+ if (!jsFileUrlReg.match(watchHtml)) return callback(null);
+
+ final url = "https://www.youtube.com" + jsFileUrlReg.matched(0);
+ httpsGet(url, {}, (status, jsFileContent) -> {
+ final funcReg = ~/function.*\.split\(""\).*\.join\(""\)}/;
+ if (!funcReg.match(jsFileContent)) return callback(null);
+
+ final decodeFunction = funcReg.matched(0);
+ final varNameReg = ~/\.split\(""\);([a-zA-Z0-9]+)\./i;
+ if (!varNameReg.match(decodeFunction)) return callback(null);
+
+ final varStartIndex = jsFileContent.indexOf("var " + varNameReg.matched(1) + "={");
+ if (varStartIndex < 0) return callback(null);
+
+ final varEndIndex = jsFileContent.indexOf("}};", varStartIndex);
+ if (varEndIndex < 0) return callback(null);
+
+ final varDeclares = jsFileContent.substring(varStartIndex, varEndIndex + 3);
+ if (varDeclares.length == 0) return callback(null);
+
+ callback(signatureCipher -> {
+ final params = new URLSearchParams(signatureCipher);
+ final obj = Object.fromEntries(params);
+ final signature = obj.s;
+ final signatureParam = obj.sp ?? "signature";
+ final url = obj.url;
+ final decodedSignature = new Function('
+ "use strict";
+ $varDeclares
+ return ($decodeFunction)("$signature");
+ ').call(null);
+ return '$url&$signatureParam=${untyped encodeURIComponent(decodedSignature)}';
+ });
+ });
+ }
+
+ public static function getInfo(url:String, callback:(info:Null<YouTubeVideoInfo>) -> Void):Void {
+ final videoId = YoutubeUtils.extractVideoId(url);
+ if (videoId.length == 0) {
+ trace("youtube videoId is not found");
+ return callback(null);
+ }
+
+ final url = 'https://www.youtube.com/watch?v=$videoId';
+ httpsGet(url, {}, (status, data) -> {
+ if (status != 200 || data.length == 0) {
+ trace("Cannot get youtube video response");
+ return callback(null);
+ }
+
+ final ytInitialPlayerResponse = resolvePlayerResponse(data);
+ final parsedResponse = Json.parse(ytInitialPlayerResponse);
+ final streamingData:YouTubeVideoInfo = parsedResponse.streamingData ?? cast {};
+ streamingData.formats ??= [];
+ streamingData.adaptiveFormats ??= [];
+ var formats:Array<YoutubeVideoFormat> = streamingData.formats.concat(streamingData.adaptiveFormats);
+
+ final promises:Array<Promise<Any>> = [];
+
+ final isEncryptedVideo = formats.exists(it -> it.signatureCipher != null);
+ if (isEncryptedVideo) {
+ final promise = new Promise((resolve, reject) -> {
+ buildDecoder(data, decoder -> {
+ if (decoder != null) {
+ formats = formats.map(item -> {
+ if (item.url != null || item.signatureCipher == null) return item;
+
+ item.url = decoder(item.signatureCipher);
+ item.signatureCipher = null;
+ return item;
+ });
+ }
+ resolve(null);
+ });
+ });
+ promises.push(promise);
+ }
+
+ Promise.all(promises).then(_ -> {
+ final result:YouTubeVideoInfo = {
+ videoDetails: parsedResponse.videoDetails ?? cast {},
+ formats: formats.filter(format -> format.url != null)
+ };
+ if (result.videoDetails.isLiveContent) {
+ final m3u8Link = resoleM3U8Link(data);
+ try {
+ result.liveData = {
+ manifestUrl: m3u8Link,
+ // data: m3u8Parser.getResult()
+ };
+ }
+ }
+ callback(result);
+ });
+ });
+ }
+}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage