SerienStream: add reCapBypass v1.0 (#1484)

This commit is contained in:
LuftVerbot
2023-04-13 17:04:57 +02:00
committed by GitHub
parent 8555791960
commit ebc2822203
4 changed files with 561 additions and 17 deletions

View File

@ -6,7 +6,7 @@ ext {
extName = 'Serienstream'
pkgNameSuffix = 'de.serienstream'
extClass = '.Serienstream'
extVersionCode = 6
extVersionCode = 7
libVersion = '13'
}

View File

@ -0,0 +1,514 @@
package eu.kanade.tachiyomi.animeextension.de.serienstream
import android.annotation.SuppressLint
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.webkit.JavascriptInterface
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class JsInterceptor : Interceptor {
private val context = Injekt.get<Application>()
private val handler by lazy { Handler(Looper.getMainLooper()) }
class JsObject(private val latch: CountDownLatch, var payload: String = "") {
@JavascriptInterface
fun passPayload(passedPayload: String) {
payload = passedPayload
latch.countDown()
}
}
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val newRequest = resolveWithWebView(originalRequest) ?: throw Exception("Please reload Episode List")
return chain.proceed(newRequest)
}
@SuppressLint("SetJavaScriptEnabled")
private fun resolveWithWebView(request: Request): Request? {
val latch = CountDownLatch(1)
var webView: WebView? = null
val origRequestUrl = request.url.toString()
val jsinterface = JsObject(latch)
// JavaSrcipt bypass recaptcha FUCK GOOGLE RECAPTCHA v1.0
val jsScript = """
function audioBufferToWav(buffer, opt) {
opt = opt || {}
var numChannels = buffer.numberOfChannels
var sampleRate = buffer.sampleRate
var format = opt.float32 ? 3 : 1
var bitDepth = format === 3 ? 32 : 16
var result
if (numChannels === 2) {
result = interleave(buffer.getChannelData(0), buffer.getChannelData(1))
} else {
result = buffer.getChannelData(0)
}
return encodeWAV(result, format, sampleRate, numChannels, bitDepth)
}
function encodeWAV(samples, format, sampleRate, numChannels, bitDepth) {
var bytesPerSample = bitDepth / 8
var blockAlign = numChannels * bytesPerSample
var buffer = new ArrayBuffer(44 + samples.length * bytesPerSample)
var view = new DataView(buffer)
/* RIFF identifier */
writeString(view, 0, 'RIFF')
/* RIFF chunk length */
view.setUint32(4, 36 + samples.length * bytesPerSample, true)
/* RIFF type */
writeString(view, 8, 'WAVE')
/* format chunk identifier */
writeString(view, 12, 'fmt ')
/* format chunk length */
view.setUint32(16, 16, true)
/* sample format (raw) */
view.setUint16(20, format, true)
/* channel count */
view.setUint16(22, numChannels, true)
/* sample rate */
view.setUint32(24, sampleRate, true)
/* byte rate (sample rate * block align) */
view.setUint32(28, sampleRate * blockAlign, true)
/* block align (channel count * bytes per sample) */
view.setUint16(32, blockAlign, true)
/* bits per sample */
view.setUint16(34, bitDepth, true)
/* data chunk identifier */
writeString(view, 36, 'data')
/* data chunk length */
view.setUint32(40, samples.length * bytesPerSample, true)
if (format === 1) { // Raw PCM
floatTo16BitPCM(view, 44, samples)
} else {
writeFloat32(view, 44, samples)
}
return buffer
}
function interleave(inputL, inputR) {
var length = inputL.length + inputR.length
var result = new Float32Array(length)
var index = 0
var inputIndex = 0
while (index < length) {
result[index++] = inputL[inputIndex]
result[index++] = inputR[inputIndex]
inputIndex++
}
return result
}
function writeFloat32(output, offset, input) {
for (var i = 0; i < input.length; i++, offset += 4) {
output.setFloat32(offset, input[i], true)
}
}
function floatTo16BitPCM(output, offset, input) {
for (var i = 0; i < input.length; i++, offset += 2) {
var s = Math.max(-1, Math.min(1, input[i]))
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true)
}
}
function writeString(view, offset, string) {
for (var i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i))
}
}
async function normalizeAudio(buffer) {
const ctx = new AudioContext();
const audioBuffer = await ctx.decodeAudioData(buffer);
ctx.close();
const offlineCtx = new OfflineAudioContext(
1,
audioBuffer.duration * 16000,
16000
);
const source = offlineCtx.createBufferSource();
source.connect(offlineCtx.destination);
source.buffer = audioBuffer;
source.start();
return offlineCtx.startRendering();
}
async function sliceAudio({
audioBuffer,
start,
end
}) {
const sampleRate = audioBuffer.sampleRate;
const channels = audioBuffer.numberOfChannels;
const startOffset = sampleRate * start;
const endOffset = sampleRate * end;
const frameCount = endOffset - startOffset;
const ctx = new AudioContext();
const audioSlice = ctx.createBuffer(channels, frameCount, sampleRate);
ctx.close();
const tempArray = new Float32Array(frameCount);
for (var channel = 0; channel < channels; channel++) {
audioBuffer.copyFromChannel(tempArray, channel, startOffset);
audioSlice.copyToChannel(tempArray, channel, 0);
}
return audioSlice;
}
async function prepareAudio(audio) {
const audioBuffer = await normalizeAudio(audio);
const audioSlice = await sliceAudio({
audioBuffer,
start: 1.5,
end: audioBuffer.duration - 1.5
});
return audioBufferToWav(audioSlice);
}
async function getWitSpeechApiResult(audioUrl) {
var audioRsp = await fetch(audioUrl);
var t = 0;
while(audioRsp.status === 404 && t <= 2){
t++;
audioRsp = await fetch(audioUrl);
}
const audioContent = await prepareAudio(await audioRsp.arrayBuffer());
const result = {};
const rsp = await fetch('https://api.wit.ai/speech?v=20221114', {
mode: 'cors',
method: 'POST',
headers: {
Authorization: 'Bearer ' + 'YZLWLZHOWH7MZR636L5IGJW66R43CEID'
},
body: new Blob([audioContent], {
type: 'audio/wav'
})
});
if (rsp.status !== 200) {
if (rsp.status === 429) {
result.errorId = 'error_apiQuotaExceeded';
result.errorTimeout = 6000;
} else {
throw new Error('API response:' + rsp.status + ',' + await rsp.text());
}
} else {
const data = JSON.parse((await rsp.text()).split('\r\n').at(-1)).text;
if (data) {
result.text = data.trim();
}
}
return result.text;
}
(async function() {
let intervalIdA = setInterval(() => {
let iframewindow = document.querySelector('iframe[title="reCAPTCHA-Aufgabe läuft in zwei Minuten ab"]').contentWindow;
if (iframewindow) {
clearInterval(intervalIdA);
let audiobutton = iframewindow.document.querySelector('#recaptcha-audio-button');
let event = iframewindow.document.createEvent('HTMLEvents');
event.initEvent('click', false, false);
audiobutton.dispatchEvent(event);
let intervalIdB = setInterval(async () => {
let source = iframewindow.document.querySelector('#audio-source').getAttribute('src');
if (source) {
clearInterval(intervalIdB);
let audioresponse = iframewindow.document.querySelector('#audio-response');
let verifybutton = iframewindow.document.querySelector('#recaptcha-verify-button');
var tries = 0;
let intervalIdC = setInterval(async () => {
var solved = null
solved = await getWitSpeechApiResult(source);
tries++;
if (solved != null) {
clearInterval(intervalIdC);
audioresponse.value = solved;
verifybutton.dispatchEvent(event);
const originalOpen = iframewindow.XMLHttpRequest.prototype.open;
iframewindow.XMLHttpRequest.prototype.open = function (method, url, async) {
if (url.includes('userverify')) {
originalOpen.apply(this, arguments); // call the original open method
this.onreadystatechange = function () {
if (this.readyState === 4 && this.status === 200) {
const responseBody = this.responseText;
window.android.passPayload(responseBody);
}
};
}
};
} else if(tries >= 2) {
window.android.passPayload("");
}
}, 2000);
}
}, 2000);
}
}, 2000);
})()
"""
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
var newRequest: Request? = null
var isDataLoaded = false
handler.post {
val webview = WebView(context)
webView = webview
with(webview.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
userAgentString = "Mozilla/5.0 (Linux; Android 12; Pixel 5 Build/SP2A.220405.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Safari/537.36"
webview.addJavascriptInterface(jsinterface, "android")
webview.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
if (request?.url.toString().contains("https://www.google.com/?token=")) {
jsinterface.payload = request?.url.toString().substringAfter("?token=").substringBefore("&original=")
latch.countDown()
}
return super.shouldInterceptRequest(view, request)
}
override fun onPageFinished(view: WebView?, url: String?) {
view?.clearCache(true)
view?.clearFormData()
val customHtml = """
<html lang="de">
<head>
<meta charset="utf-8">
<title>Überprüfung</title>
<meta name="robots" content="noindex, nofollow">
<meta name="description" content="S.to Stream Weiterleitung">
<meta name="keywords" content="S.to, serien stream, online stream, serien kostenlos, serien gratis, serien deutsch kostenlos, s.to, serienstream, serien gratis, kino, stream, maxdome kostenlos, netflix kostenlos, kinox.to, Android Stream, kinox.to alternative, movie2k, iPad Stream, movie4k, burning series, burning-seri.es, iphone stream, burning series app, burning series down, mobile stream, burning series serien, Onlineserien">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="/favicon.ico">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<link rel="apple-touch-icon" href="https://zrt5351b7er9.static-webarchive.org/img/touch-icon-iphone.png">
<link rel="apple-touch-icon" sizes="76x76" href="https://zrt5351b7er9.static-webarchive.org/img/touch-icon-ipad.png">
<link rel="apple-touch-icon" sizes="120x120" href="https://zrt5351b7er9.static-webarchive.org/img/touch-icon-iphone-retina.png">
<link rel="apple-touch-icon" sizes="152x152" href="https://zrt5351b7er9.static-webarchive.org/img/touch-icon-ipad-retina.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<script type="text/javascript" src="https://zrt5351b7er9.static-webarchive.org/js/jquery.min.js"></script>
<script type="text/javascript" src="https://zrt5351b7er9.static-webarchive.org/js/jquery-ui.min.js"></script>
<!--[if IE]>
<script src="https://zrt5351b7er9.static-webarchive.org/js/html5shiv.min.js"></script>
<script src="https://zrt5351b7er9.static-webarchive.org/js/respond.min.js"></script><![endif]-->
<script>
var init = function () {
grecaptcha.render('captcha', {
'sitekey': '6LeBCHsaAAAAABiuOuoPJ_0E1Ny6OF5mAnuqDoK7', 'size': 'invisible', 'callback': securitCaptcha
});
grecaptcha.execute();
};
var securitCaptcha = function (token) {
${'$'}('.securityToken').val(token);
${'$'}('.securityTokenForm').submit();
};
</script>
<style type="text/css">
@import url(//fonts.googleapis.com/css?family=Open+Sans:400,600,700);
html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, menu, nav, output, ruby, section, summary, time, mark, audio, video {
margin: 0;
padding: 0 0 0 0;
border: 0;
vertical-align: baseline;
box-sizing: border-box;
}
html {
line-height: 1;
font-family: 'Open Sans', 'Helvetica Neue', helvetica, arial, sans-serif;
background: #fafafa;
}
html,
body {
height: 100%;
text-align: center;
}
body {
background: #0f1620;
height: 100%;
}
h1, p, small {
text-align: center;
}
h1 {
color: #fff !important;
font-size: 200%;
}
small {
margin-top: 50px;
color: #fff !important;
display: inline-block;
opacity: 0.7;
}
a:hover {
color: #159cf5;
!important;
}
p {
color: #fff !important;
font-size: 150%;
margin-top: 20px;
}
.logo-wrapper {
width: 100%;
text-align: center;
margin-bottom: 65px;
}
.logo-wrapper > a {
text-align: center;
}
.logo-wrapper > a > .header-logo {
float: none;
display: inline-block;
}
.sk-double-bounce {
width: 80px;
height: 80px;
position: relative;
margin: 100px auto;
}
.sk-double-bounce .sk-child {
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #44adf3;
opacity: 0.6;
position: absolute;
top: 0;
left: 0;
-webkit-animation: sk-doubleBounce 2s infinite ease-in-out;
animation: sk-doubleBounce 2s infinite ease-in-out;
}
.sk-double-bounce .sk-double-bounce2 {
-webkit-animation-delay: -1.0s;
animation-delay: -1.0s;
}
@-webkit-keyframes sk-doubleBounce {
0%, 100% {
-webkit-transform: scale(0);
transform: scale(0);
}
50% {
-webkit-transform: scale(1);
transform: scale(1);
}
}
@keyframes sk-doubleBounce {
0%, 100% {
-webkit-transform: scale(0);
transform: scale(0);
}
50% {
-webkit-transform: scale(1);
transform: scale(1);
}
}
</style>
</head>
<body>
<div id="wrapper">
<div class="container" style="padding: 10px">
<form method="GET" class="securityTokenForm">
<input type="hidden" name="token" class="securityToken"/>
<input type="hidden" name="original"
value=""/>
</form>
<h1>Dein Stream wird überprüft...</h1>
<p>Falls nötig, löse bitte das Captcha, um die Serie weiterschauen zu können.</p>
<div class="sk-double-bounce">
<div class="sk-child sk-double-bounce1"></div>
<div class="sk-child sk-double-bounce2"></div>
</div>
<div id="captcha"></div>
<script src="https://www.google.com/recaptcha/api.js?onload=init&render=explicit" async defer></script>
</div>
</div>
</body>
</html>
"""
if (!isDataLoaded) {
view?.loadDataWithBaseURL("https://www.google.com/", customHtml, "text/html", "UTF-8", null)
isDataLoaded = true
}
view?.evaluateJavascript(jsScript) {}
}
}
webView?.loadUrl(origRequestUrl, headers)
}
}
latch.await(60, TimeUnit.SECONDS)
handler.post {
webView?.stopLoading()
webView?.destroy()
webView = null
}
newRequest = GET(request.url.toString(), headers = Headers.headersOf("url", jsinterface.payload.substringAfter("uvresp\",\"").substringBefore("\",")))
return newRequest
}
}

View File

@ -26,7 +26,7 @@ class RedirectInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val newRequest = resolveWithWebView(originalRequest) ?: throw Exception("Bitte Captcha in WebView lösen")
val newRequest = resolveWithWebView(originalRequest) ?: throw Exception("Versuche es später nochmal")
return chain.proceed(newRequest)
}
@ -40,10 +40,13 @@ class RedirectInterceptor : Interceptor {
var webView: WebView? = null
val origRequestUrl = request.url.toString()
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
var newRequest: Request? = null
var test = true
handler.post {
val webview = WebView(context)
webView = webview
@ -53,9 +56,7 @@ class RedirectInterceptor : Interceptor {
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
userAgentString = // request.header("User-Agent")
// ?: "\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63\""
"Mozilla/5.0 (Linux; Android 12; Pixel 5 Build/SP2A.220405.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.127 Safari/537.36"
userAgentString = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:106.0) Gecko/20100101 Firefox/106.0"
}
webview.webViewClient = object : WebViewClient() {
@ -63,17 +64,26 @@ class RedirectInterceptor : Interceptor {
view: WebView,
request: WebResourceRequest,
): WebResourceResponse? {
if (request.url.toString().contains("token") || request.url.toString().contains("https://dood.") ||
request.url.toString().contains("https://streamtape") || request.url.toString().contains("https://voe")
) {
if (request.url.toString().contains("payload")) {
newRequest = GET(request.url.toString(), request.requestHeaders.toHeaders())
latch.countDown()
} else if (request.url.toString().contains("https://s.to/redirect/") && request.url.toString().contains("token")) {
newRequest = GET(request.url.toString(), request.requestHeaders.toHeaders())
latch.countDown()
} else {
test = false
}
if (test == false) {
newRequest = GET(origRequestUrl, headers.toHeaders())
latch.countDown()
}
return super.shouldInterceptRequest(view, request)
}
}
webView?.loadUrl(origRequestUrl, headers)
}
// Wait a reasonable amount of time to retrieve the solution. The minimum should be
// around 4 seconds but it can take more due to slow networks or server issues.
latch.await(12, TimeUnit.SECONDS)

View File

@ -227,31 +227,45 @@ class Serienstream : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val videoList = mutableListOf<Video>()
val hosterSelection = preferences.getStringSet(SConstants.HOSTER_SELECTION, null)
val redirectInterceptor = client.newBuilder().addInterceptor(RedirectInterceptor()).build()
val jsInterceptor = client.newBuilder().addInterceptor(JsInterceptor()).build()
redirectlink.forEach {
val langkey = it.attr("data-lang-key")
val language = getlanguage(langkey)
val redirectgs = baseUrl + it.selectFirst("a.watchEpisode")!!.attr("href")
val redirects = redirectInterceptor.newCall(GET(redirectgs)).execute().request.url.toString()
val hoster = it.select("a h4").text()
if (hosterSelection != null) {
when {
redirects.contains("https://voe.sx") || redirects.contains("https://launchreliantcleaverriver") ||
redirects.contains("https://fraudclatterflyingcar") && hosterSelection.contains(SConstants.NAME_VOE) -> {
hoster.contains("VOE") && hosterSelection.contains(SConstants.NAME_VOE) -> {
val quality = "Voe $language"
val video = VoeExtractor(client).videoFromUrl(redirects, quality)
var url = redirectInterceptor.newCall(GET(redirectgs)).execute().request.url.toString()
if (url.contains("payload") || url.contains(redirectgs)) {
url = recapbypass(jsInterceptor, redirectgs)
}
val video = VoeExtractor(client).videoFromUrl(url, quality)
if (video != null) {
videoList.add(video)
}
}
redirects.contains("https://dood") && hosterSelection.contains(SConstants.NAME_DOOD) -> {
hoster.contains("Doodstream") && hosterSelection.contains(SConstants.NAME_DOOD) -> {
val quality = "Doodstream $language"
val video = DoodExtractor(client).videoFromUrl(redirects, quality)
var url = redirectInterceptor.newCall(GET(redirectgs)).execute().request.url.toString()
if (url.contains("payload") || url.contains(redirectgs)) {
url = recapbypass(jsInterceptor, redirectgs)
}
val video = DoodExtractor(client).videoFromUrl(url, quality)
if (video != null) {
videoList.add(video)
}
}
redirects.contains("https://streamtape") && hosterSelection.contains(SConstants.NAME_STAPE) -> {
hoster.contains("Streamtape") && hosterSelection.contains(SConstants.NAME_STAPE) -> {
val quality = "Streamtape $language"
val video = StreamTapeExtractor(client).videoFromUrl(redirects, quality)
var url = redirectInterceptor.newCall(GET(redirectgs)).execute().request.url.toString()
if (url.contains("payload") || url.contains(redirectgs)) {
url = recapbypass(jsInterceptor, redirectgs)
}
val video = StreamTapeExtractor(client).videoFromUrl(url, quality)
if (video != null) {
videoList.add(video)
}
@ -262,6 +276,12 @@ class Serienstream : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return videoList
}
private fun recapbypass(jsInterceptor: OkHttpClient, redirectgs: String): String {
val token = jsInterceptor.newCall(GET(redirectgs)).execute().request.header("url").toString()
val url = client.newCall(GET("$redirectgs?token=$token&original=")).execute().request.url.toString()
return url
}
private fun getlanguage(langkey: String): String? {
when {
langkey.contains("${SConstants.KEY_GER_SUB}") -> {
@ -392,7 +412,7 @@ class Serienstream : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
Toast.makeText(context, "Starte Aniyomi neu, um die Einstellungen zu übernehmen.", Toast.LENGTH_LONG).show()
res
} catch (e: Exception) {
Log.e("Anicloud", "Fehler beim festlegen der Einstellung.", e)
Log.e("SerienStream", "Fehler beim festlegen der Einstellung.", e)
false
}
}