Connect poToken generation to extractor

This commit is contained in:
Stypox 2025-01-26 12:28:09 +01:00 committed by AudricV
parent 690b3410e9
commit 6010c4ea7f
No known key found for this signature in database
GPG key ID: DA92EC7905614198
5 changed files with 125 additions and 59 deletions

View file

@ -3,15 +3,7 @@ package org.schabi.newpipe;
import android.app.Application; import android.app.Application;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Build;
import android.os.UserManager;
import android.util.Log; import android.util.Log;
import android.view.View;
import android.webkit.JavascriptInterface;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.RelativeLayout;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.app.NotificationChannelCompat; import androidx.core.app.NotificationChannelCompat;
@ -25,8 +17,7 @@ import org.acra.config.CoreConfigurationBuilder;
import org.schabi.newpipe.error.ReCaptchaActivity; import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.downloader.Downloader; import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.services.youtube.PoTokenResult; import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.BridgeStateSaverInitializer; import org.schabi.newpipe.util.BridgeStateSaverInitializer;
@ -36,7 +27,7 @@ import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.image.ImageStrategy; import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper; import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.image.PreferredImageQuality; import org.schabi.newpipe.util.image.PreferredImageQuality;
import org.schabi.newpipe.util.potoken.PoTokenWebView; import org.schabi.newpipe.util.potoken.PoTokenProviderImpl;
import java.io.IOException; import java.io.IOException;
import java.io.InterruptedIOException; import java.io.InterruptedIOException;
@ -44,16 +35,12 @@ import java.net.SocketException;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.exceptions.CompositeException; import io.reactivex.rxjava3.exceptions.CompositeException;
import io.reactivex.rxjava3.exceptions.MissingBackpressureException; import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException; import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
import io.reactivex.rxjava3.exceptions.UndeliverableException; import io.reactivex.rxjava3.exceptions.UndeliverableException;
import io.reactivex.rxjava3.functions.Consumer; import io.reactivex.rxjava3.functions.Consumer;
import io.reactivex.rxjava3.plugins.RxJavaPlugins; import io.reactivex.rxjava3.plugins.RxJavaPlugins;
import kotlin.Pair;
/* /*
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org> * Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
@ -134,22 +121,7 @@ public class App extends Application {
configureRxJavaErrorHandler(); configureRxJavaErrorHandler();
CompositeDisposable disposable = new CompositeDisposable(); YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl.INSTANCE);
disposable.add(PoTokenWebView.Companion.newPoTokenGenerator(this)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(AndroidSchedulers.mainThread())
.flatMap(poTokenGenerator -> Single.zip(
poTokenGenerator.generatePoToken(YoutubeParsingHelper
.randomVisitorData(NewPipe.getPreferredContentCountry())),
poTokenGenerator.generatePoToken("i_SsnRdgitA"),
Pair::new
))
.subscribe(
pots -> Log.e(TAG, "success! " + pots.getSecond().poToken +
",web.gvs+" + pots.getFirst().poToken +
";visitor_data=" + pots.getFirst().visitorData),
error -> Log.e(TAG, "error", error)
));
} }
@Override @Override

View file

@ -17,6 +17,7 @@ import android.view.InputDevice;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.WindowInsets; import android.view.WindowInsets;
import android.view.WindowManager; import android.view.WindowManager;
import android.webkit.CookieManager;
import androidx.annotation.Dimension; import androidx.annotation.Dimension;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -335,4 +336,17 @@ public final class DeviceUtils {
&& !TX_50JXW834 && !TX_50JXW834
&& !HMB9213NW; && !HMB9213NW;
} }
/**
* @return whether the device has support for WebView, see
* <a href="https://stackoverflow.com/a/69626735">https://stackoverflow.com/a/69626735</a>
*/
public static boolean supportsWebView() {
try {
CookieManager.getInstance();
return true;
} catch (Throwable ignored) {
return false;
}
}
} }

View file

@ -2,16 +2,25 @@ package org.schabi.newpipe.util.potoken
import android.content.Context import android.content.Context
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import org.schabi.newpipe.extractor.services.youtube.PoTokenResult
import java.io.Closeable import java.io.Closeable
/**
* This interface was created to allow for multiple methods to generate poTokens in the future (e.g.
* via WebView and via a local DOM implementation)
*/
interface PoTokenGenerator : Closeable { interface PoTokenGenerator : Closeable {
/** /**
* Generates a poToken for the provided identifier, using the `integrityToken` and * Generates a poToken for the provided identifier, using the `integrityToken` and
* `webPoSignalOutput` previously obtained in the initialization of [PoTokenWebView]. Can be * `webPoSignalOutput` previously obtained in the initialization of [PoTokenWebView]. Can be
* called multiple times. * called multiple times.
*/ */
fun generatePoToken(identifier: String): Single<PoTokenResult> fun generatePoToken(identifier: String): Single<String>
/**
* @return whether the `integrityToken` is expired, in which case all tokens generated by
* [generatePoToken] will be invalid
*/
fun isExpired(): Boolean
interface Factory { interface Factory {
/** /**

View file

@ -0,0 +1,59 @@
package org.schabi.newpipe.util.potoken
import android.util.Log
import org.schabi.newpipe.App
import org.schabi.newpipe.extractor.NewPipe
import org.schabi.newpipe.extractor.services.youtube.PoTokenProvider
import org.schabi.newpipe.extractor.services.youtube.PoTokenResult
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper
import org.schabi.newpipe.util.DeviceUtils
object PoTokenProviderImpl : PoTokenProvider {
val TAG = PoTokenProviderImpl::class.simpleName
private val webViewSupported by lazy { DeviceUtils.supportsWebView() }
private object WebPoTokenGenLock
private var webPoTokenVisitorData: String? = null
private var webPoTokenStreamingPot: String? = null
private var webPoTokenGenerator: PoTokenGenerator? = null
override fun getWebClientPoToken(videoId: String): PoTokenResult? {
if (!webViewSupported) {
return null
}
val (poTokenGenerator, visitorData, streamingPot) = synchronized(WebPoTokenGenLock) {
if (webPoTokenGenerator == null || webPoTokenGenerator!!.isExpired()) {
webPoTokenGenerator = PoTokenWebView.newPoTokenGenerator(App.getApp()).blockingGet()
webPoTokenVisitorData = YoutubeParsingHelper
.randomVisitorData(NewPipe.getPreferredContentCountry())
// The streaming poToken needs to be generated exactly once before generating any
// other (player) tokens.
webPoTokenStreamingPot = webPoTokenGenerator!!
.generatePoToken(webPoTokenVisitorData!!).blockingGet()
}
return@synchronized Triple(
webPoTokenGenerator!!, webPoTokenVisitorData!!, webPoTokenStreamingPot!!
)
}
// Not using synchronized here, since poTokenGenerator would be able to generate multiple
// poTokens in parallel if needed. The only important thing is for exactly one
// visitorData/streaming poToken to be generated before anything else.
val playerPot = poTokenGenerator.generatePoToken(videoId).blockingGet()
Log.e(TAG, "success($videoId) $playerPot,web.gvs+$streamingPot;visitor_data=$visitorData")
return PoTokenResult(
webPoTokenVisitorData!!,
playerPot,
webPoTokenStreamingPot!!,
)
}
override fun getWebEmbedClientPoToken(videoId: String): PoTokenResult? = null
override fun getAndroidClientPoToken(videoId: String): PoTokenResult? = null
override fun getIosClientPoToken(videoId: String): PoTokenResult? = null
}

View file

@ -7,14 +7,13 @@ import android.os.Looper
import android.util.Log import android.util.Log
import android.webkit.JavascriptInterface import android.webkit.JavascriptInterface
import android.webkit.WebView import android.webkit.WebView
import androidx.annotation.MainThread
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.core.SingleEmitter import io.reactivex.rxjava3.core.SingleEmitter
import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.DownloaderImpl import org.schabi.newpipe.DownloaderImpl
import org.schabi.newpipe.extractor.services.youtube.PoTokenResult import java.time.Instant
class PoTokenWebView private constructor( class PoTokenWebView private constructor(
context: Context, context: Context,
@ -23,7 +22,8 @@ class PoTokenWebView private constructor(
) : PoTokenGenerator { ) : PoTokenGenerator {
private val webView = WebView(context) private val webView = WebView(context)
private val disposables = CompositeDisposable() // used only during initialization private val disposables = CompositeDisposable() // used only during initialization
private val poTokenEmitters = mutableListOf<Pair<String, SingleEmitter<PoTokenResult>>>() private val poTokenEmitters = mutableListOf<Pair<String, SingleEmitter<String>>>()
private lateinit var expirationInstant: Instant
//region Initialization //region Initialization
init { init {
@ -114,11 +114,12 @@ class PoTokenWebView private constructor(
"https://jnn-pa.googleapis.com/\$rpc/google.internal.waa.v1.Waa/GenerateIT", "https://jnn-pa.googleapis.com/\$rpc/google.internal.waa.v1.Waa/GenerateIT",
"[ \"$REQUEST_KEY\", \"$botguardResponse\" ]", "[ \"$REQUEST_KEY\", \"$botguardResponse\" ]",
) { responseBody -> ) { responseBody ->
Log.e(TAG, "GenerateIT response: $responseBody")
webView.evaluateJavascript( webView.evaluateJavascript(
"""(async function() { """(async function() {
try { try {
globalThis.integrityToken = JSON.parse(String.raw`$responseBody`) globalThis.integrityToken = JSON.parse(String.raw`$responseBody`)
PoTokenWebView.onInitializationFinished() PoTokenWebView.onInitializationFinished(integrityToken[1])
} catch (error) { } catch (error) {
PoTokenWebView.onJsInitializationError(error.toString()) PoTokenWebView.onJsInitializationError(error.toString())
} }
@ -130,9 +131,14 @@ class PoTokenWebView private constructor(
/** /**
* Called during initialization by the JavaScript snippet from [onRunBotguardResult] when the * Called during initialization by the JavaScript snippet from [onRunBotguardResult] when the
* `integrityToken` has been received by JavaScript. * `integrityToken` has been received by JavaScript.
*
* @param expirationTimeInSeconds in how many seconds the integrity token expires, can be found
* in `integrityToken[1]`
*/ */
@JavascriptInterface @JavascriptInterface
fun onInitializationFinished() { fun onInitializationFinished(expirationTimeInSeconds: Long) {
// leave 10 minutes of margin just to be sure
expirationInstant = Instant.now().plusSeconds(expirationTimeInSeconds - 600)
generatorEmitter.onSuccess(this) generatorEmitter.onSuccess(this)
} }
//endregion //endregion
@ -143,7 +149,7 @@ class PoTokenWebView private constructor(
* multiple poToken requests can be generated invparallel, and the results will be notified to * multiple poToken requests can be generated invparallel, and the results will be notified to
* the right emitters. * the right emitters.
*/ */
private fun addPoTokenEmitter(identifier: String, emitter: SingleEmitter<PoTokenResult>) { private fun addPoTokenEmitter(identifier: String, emitter: SingleEmitter<String>) {
synchronized(poTokenEmitters) { synchronized(poTokenEmitters) {
poTokenEmitters.add(Pair(identifier, emitter)) poTokenEmitters.add(Pair(identifier, emitter))
} }
@ -154,7 +160,7 @@ class PoTokenWebView private constructor(
* [identifier]. The emitter is supposed to be used immediately after to either signal a success * [identifier]. The emitter is supposed to be used immediately after to either signal a success
* or an error. * or an error.
*/ */
private fun popPoTokenEmitter(identifier: String): SingleEmitter<PoTokenResult>? { private fun popPoTokenEmitter(identifier: String): SingleEmitter<String>? {
return synchronized(poTokenEmitters) { return synchronized(poTokenEmitters) {
poTokenEmitters.indexOfFirst { it.first == identifier }.takeIf { it >= 0 }?.let { poTokenEmitters.indexOfFirst { it.first == identifier }.takeIf { it >= 0 }?.let {
poTokenEmitters.removeAt(it).second poTokenEmitters.removeAt(it).second
@ -162,22 +168,23 @@ class PoTokenWebView private constructor(
} }
} }
@MainThread override fun generatePoToken(identifier: String): Single<String> =
override fun generatePoToken(identifier: String): Single<PoTokenResult> =
Single.create { emitter -> Single.create { emitter ->
addPoTokenEmitter(identifier, emitter) addPoTokenEmitter(identifier, emitter)
Handler(Looper.getMainLooper()).post {
webView.evaluateJavascript( webView.evaluateJavascript(
"""(async function() { """(async function() {
identifier = String.raw`$identifier` identifier = String.raw`$identifier`
try { try {
poToken = await obtainPoToken(webPoSignalOutput, integrityToken, identifier) poToken = await obtainPoToken(webPoSignalOutput, integrityToken,
PoTokenWebView.onObtainPoTokenResult(identifier, poToken) identifier)
} catch (error) { PoTokenWebView.onObtainPoTokenResult(identifier, poToken)
PoTokenWebView.onObtainPoTokenError(identifier, error.toString()) } catch (error) {
} PoTokenWebView.onObtainPoTokenError(identifier, error.toString())
})();""", }
) {} })();""",
) {}
}
} }
/** /**
@ -198,7 +205,11 @@ class PoTokenWebView private constructor(
fun onObtainPoTokenResult(identifier: String, poToken: String) { fun onObtainPoTokenResult(identifier: String, poToken: String) {
Log.e(TAG, "identifier=$identifier") Log.e(TAG, "identifier=$identifier")
Log.e(TAG, "poToken=$poToken") Log.e(TAG, "poToken=$poToken")
popPoTokenEmitter(identifier)?.onSuccess(PoTokenResult(identifier, poToken)) popPoTokenEmitter(identifier)?.onSuccess(poToken)
}
override fun isExpired(): Boolean {
return Instant.now().isAfter(expirationInstant)
} }
//endregion //endregion
@ -286,12 +297,13 @@ class PoTokenWebView private constructor(
private const val REQUEST_KEY = "O43z0dpjhgX20SCx4KAo" private const val REQUEST_KEY = "O43z0dpjhgX20SCx4KAo"
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.3" private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.3"
@MainThread
override fun newPoTokenGenerator(context: Context): Single<PoTokenGenerator> = override fun newPoTokenGenerator(context: Context): Single<PoTokenGenerator> =
Single.create { emitter -> Single.create { emitter ->
val potWv = PoTokenWebView(context, emitter) Handler(Looper.getMainLooper()).post {
potWv.loadHtmlAndObtainBotguard(context) val potWv = PoTokenWebView(context, emitter)
emitter.setDisposable(potWv.disposables) potWv.loadHtmlAndObtainBotguard(context)
emitter.setDisposable(potWv.disposables)
}
} }
} }
} }