Skip to content

Commit

Permalink
Use different method for CORS bypass
Browse files Browse the repository at this point in the history
Instruct webview to lie that it is `youtube.com`
Pros:
- local api streaming works without `file://` url flag
Cons:
- video urls fetched from invidious are still blocked by CORS
- some urls don't allow youtube.com to fetch them and need to be bypassed by a javascript interface
- it completely breaks browser navigation so that has to be reimplemented in freetube

#5
  • Loading branch information
MarmadileManteater committed Jan 30, 2024
1 parent d23df55 commit 9f3914b
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import android.webkit.JavascriptInterface
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationManagerCompat
import org.json.JSONObject
import org.json.JSONTokener
import java.net.HttpURLConnection
import java.net.URL
class FreeTubeJavaScriptInterface {
private var context: MainActivity
Expand Down Expand Up @@ -185,4 +188,21 @@ class FreeTubeJavaScriptInterface {
fun updateMediaSessionData(trackName: String, artist: String, duration: Long, art: String? = null) {
setMetadata(mediaSession!!, trackName, artist, duration, art)
}

@JavascriptInterface
fun googleSuggestions(url: String, method: String, headers: String): String {
val jsonObject = JSONTokener(headers).nextValue() as JSONObject
val headersMap = mutableMapOf<String, String>()
for (key in jsonObject.keys()) {
headersMap[key] = jsonObject.getString(key)
}
// this needs to be requested at the os level
val connection = URL(url).openConnection() as HttpURLConnection
for (header in headersMap) {
connection.setRequestProperty(header.key, header.value)
}
connection.requestMethod = method
connection.connect()
return connection.inputStream.bufferedReader().use { it.readText() }
}
}
46 changes: 42 additions & 4 deletions android/app/src/main/java/io/freetubeapp/freetube/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import android.os.Looper
import android.view.View
import android.view.ViewGroup
import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.FrameLayout
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity
Expand All @@ -25,6 +30,7 @@ class MainActivity : AppCompatActivity(), OnRequestPermissionsResultCallback {
private lateinit var keepAliveIntent: Intent
private var fullscreenView: View? = null
lateinit var webView: BackgroundPlayWebView

@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -51,17 +57,18 @@ class MainActivity : AppCompatActivity(), OnRequestPermissionsResultCallback {
// bind the back button to the web-view history
onBackPressedDispatcher.addCallback {
if (webView.canGoBack()) {
webView.goBack()
webView.loadUrl("javascript: window.history.goBack()")
} else {
this@MainActivity.moveTaskToBack(true)
}
}

webView.settings.javaScriptEnabled = true
webView.settings.cacheMode = WebSettings.LOAD_NO_CACHE

// this is the 🥃 special sauce that makes local api streaming a possibility
webView.settings.allowUniversalAccessFromFileURLs = true
webView.settings.allowFileAccessFromFileURLs = true
// webView.settings.allowUniversalAccessFromFileURLs = true
// webView.settings.allowFileAccessFromFileURLs = true

val jsInterface = FreeTubeJavaScriptInterface(this)
webView.addJavascriptInterface(jsInterface, "Android")
Expand All @@ -84,7 +91,38 @@ class MainActivity : AppCompatActivity(), OnRequestPermissionsResultCallback {
this@MainActivity.binding.root.fitsSystemWindows = true
}
}
webView.loadUrl("file:///android_asset/index.html")
// webView.loadUrl("file:///android_asset/index.html")
val htmlFile = assets.open("index.html").bufferedReader().use { it.readText() }
webView.webViewClient = object: WebViewClient() {
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?
): WebResourceResponse? {
// we are pretending to be youtube.com, so we need to intercept requests for our local resources
if (request!!.url.toString().startsWith("https://www.youtube.com/")) {
try {
val mimeType = if (request!!.url.toString().endsWith(".css")) {
"text/css"
} else if (request!!.url.toString().endsWith(".js")) {
"application/javascript"
} else {
"text/plain"
}
val asset = assets.open(request!!.url.path!!.substring(1))
return WebResourceResponse(
mimeType,
"utf-8",
asset
)
} catch (e: Exception) {
println(e.message)
}
}
return super.shouldInterceptRequest(view, request)
}
}

webView.loadDataWithBaseURL("https://www.youtube.com/", htmlFile, "text/html", "utf-8", "file://android_asset/index.html")
Handler(Looper.getMainLooper()).postDelayed({
webView.loadUrl(
"javascript: window.mediaSessionListeners = {};" +
Expand Down
57 changes: 39 additions & 18 deletions src/renderer/components/top-nav/top-nav.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,37 +301,58 @@ export default defineComponent({
},

navigateHistory: function () {
if (!this.isForwardOrBack) {
this.historyIndex = window.history.length
this.isArrowBackwardDisabled = false
this.isArrowForwardDisabled = true
if (!process.env.IS_ANDROID) {
if (!this.isForwardOrBack) {
this.historyIndex = window.history.length
this.isArrowBackwardDisabled = false
this.isArrowForwardDisabled = true
} else {
this.isForwardOrBack = false
}
} else {
this.isForwardOrBack = false
this.isArrowForwardDisabled = !this.$router.canGoForward()
this.isArrowBackwardDisabled = !this.$router.canGoBackward()
}
},

historyBack: function () {
this.isForwardOrBack = true
window.history.back()

if (this.historyIndex > 1) {
this.historyIndex--
this.isArrowForwardDisabled = false
if (this.historyIndex === 1) {
if (!process.env.IS_ANDROID) {
this.isForwardOrBack = true
window.history.back()

if (this.historyIndex > 1) {
this.historyIndex--
this.isArrowForwardDisabled = false
if (this.historyIndex === 1) {
this.isArrowBackwardDisabled = true
}
}
} else {
if (this.$router.back()) {
this.isArrowForwardDisabled = false
} else {
this.isArrowBackwardDisabled = true
}
}
},

historyForward: function () {
this.isForwardOrBack = true
window.history.forward()
if (!process.env.IS_ANDROID) {
this.isForwardOrBack = true
window.history.forward()

if (this.historyIndex < window.history.length) {
this.historyIndex++
this.isArrowBackwardDisabled = false
if (this.historyIndex < window.history.length) {
this.historyIndex++
this.isArrowBackwardDisabled = false

if (this.historyIndex === window.history.length) {
if (this.historyIndex === window.history.length) {
this.isArrowForwardDisabled = true
}
}
} else {
if (this.$router.forward()) {
this.isArrowBackwardDisabled = false
} else {
this.isArrowForwardDisabled = true
}
}
Expand Down
21 changes: 20 additions & 1 deletion src/renderer/helpers/api/local.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getUserDataPath,
toLocalePublicationString
} from '../utils'
import android from 'android'

const TRACKING_PARAM_NAMES = [
'utm_source',
Expand Down Expand Up @@ -53,7 +54,25 @@ async function createInnertube(options = { withPlayer: false, location: undefine
client_type: options.clientType,

// use browser fetch
fetch: (input, init) => fetch(input, init),
fetch: (input, init) => {
if (process.env.IS_ANDROID) {
const serializeHeaders = (headers) => Object.fromEntries(Array.from(headers.keys()).map((key) => [key, headers.get(key)]))
if (process.env.IS_ANDROID) {
// this url isn't allowed when fetching from YT, and the only way out is through (the javascript interface)
if (input.url?.startsWith('https://suggestqueries.google.com/')) {
const stringResponse = android.googleSuggestions(input.url, input.method, JSON.stringify({ ...serializeHeaders(input.headers), ...serializeHeaders(init.headers) }))
return {
text() {
return stringResponse
},
ok: true,
status: 200
}
}
}
}
return fetch(input, init)
},
cache,
generate_session_locally: !!options.generateSessionLocally
})
Expand Down
36 changes: 36 additions & 0 deletions src/renderer/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,33 @@ const router = new Router({

const originalPush = router.push.bind(router)

const routes = [{ path: '/' }]
let historyIndex = 0
if (process.env.IS_ANDROID) {
// android webview sacrifices history for tricking the webview into thinkings its youtube
router.back = () => {
historyIndex--
originalPush(routes[historyIndex])
return router.canGoBackward()
}

router.canGoBackward = () => {
return historyIndex - 1 >= 0
}

router.forward = () => {
historyIndex++
originalPush(routes[historyIndex])
return router.canGoForward()
}

router.canGoForward = () => {
return historyIndex + 1 < routes.length
}

window.history.goBack = router.back
}

router.push = (location) => {
// only navigates if the location is not identical to the current location

Expand All @@ -176,6 +203,15 @@ router.push = (location) => {
const queriesAreDiff = newQueryUSP.toString() !== currentQueryUSP.toString()

if (pathsAreDiff || queriesAreDiff) {
if (process.env.IS_ANDROID) {
// if the route history is currently not all the way in the present
if (historyIndex !== routes.length - 1) {
// remove all future history
routes.splice(historyIndex + 1, routes.length - historyIndex)
}
routes.push(location)
historyIndex++
}
return originalPush(location)
}
}
Expand Down

0 comments on commit 9f3914b

Please sign in to comment.