Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions apps/app-frontend/src/pages/project/Description.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,35 @@
<template>
<Card>
<ProjectPageDescription :description="project.body" />
<div class="markdown-body" v-html="descriptionHtml" />
</Card>
</template>

<script setup>
import { Card, ProjectPageDescription } from '@modrinth/ui'
import { Card } from '@modrinth/ui'
import { renderHighlightedString } from '@modrinth/utils'
import { invoke } from '@tauri-apps/api/core'
import { computedAsync } from '@vueuse/core'

defineProps({
const props = defineProps({
project: {
type: Object,
default: () => {},
},
})

const youtubeFrame =
/(<iframe\b[^>]*\bsrc=")https?:\/\/(?:www\.)?youtube(?:-nocookie)?\.com\/embed\/([a-zA-Z0-9_-]{11})(?=[?#"])/g
const descriptionHtml = computedAsync(async () => {
const html = renderHighlightedString(props.project?.body ?? '')
if (!html.match(youtubeFrame)) return html
try {
const origin = await invoke('youtube_embed_origin')
return html.replace(youtubeFrame, `$1${origin}/$2`)
} catch (error) {
console.error('Failed to initialize YouTube embeds', error)
return html
}
}, '')
</script>

<script>
Expand Down
5 changes: 5 additions & 0 deletions apps/app/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>127.0.0.1</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true />
</dict>
<key>asset.localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
Expand Down
1 change: 1 addition & 0 deletions apps/app/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub mod cache;
pub mod files;
pub mod friends;
pub mod worlds;
pub mod youtube;

mod oauth_utils;

Expand Down
87 changes: 87 additions & 0 deletions apps/app/src/api/youtube.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use std::time::Duration;

use hyper::{Method, Request, Response, StatusCode, body::Incoming};
use hyper_util::rt::TokioIo;
use tokio::{net::TcpListener, sync::OnceCell};
use url::Url;

static EMBED_ORIGIN: OnceCell<String> = OnceCell::const_new();

#[tauri::command]
pub async fn youtube_embed_origin() -> super::Result<String> {
let origin = EMBED_ORIGIN
.get_or_try_init(|| async {
let listener = TcpListener::bind(("127.0.0.1", 0)).await?;
let origin = format!("http://{}", listener.local_addr()?);
tauri::async_runtime::spawn(async move {
loop {
let socket = match listener.accept().await {
Ok((socket, _)) => socket,
Err(error) => {
tracing::warn!(
"Failed to accept YouTube embed connection: {error}"
);
tokio::time::sleep(Duration::from_secs(1)).await;
continue;
}
};
tauri::async_runtime::spawn(async move {
let connection =
hyper::server::conn::http1::Builder::new()
.keep_alive(false)
.serve_connection(
TokioIo::new(socket),
hyper::service::service_fn(embed_response),
);
let _ = tokio::time::timeout(
Duration::from_secs(5),
connection,
)
.await;
});
}
});
Ok::<_, std::io::Error>(origin)
})
.await?;
Ok(origin.clone())
}

async fn embed_response(
request: Request<Incoming>,
) -> Result<Response<String>, hyper::http::Error> {
let uri = request.uri();
let id = uri.path().strip_prefix('/').unwrap_or_default();
if request.method() != Method::GET
|| id.len() != 11
|| !id.bytes().all(|byte| {
byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-'
}) {
return Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(String::new());
}

let mut url =
Url::parse(&format!("https://www.youtube-nocookie.com/embed/{id}"))
.unwrap();
for (key, value) in
url::form_urlencoded::parse(uri.query().unwrap_or_default().as_bytes())
{
if matches!(key.as_ref(), "start" | "end")
&& !value.is_empty()
&& value.bytes().all(|byte| byte.is_ascii_digit())
{
url.query_pairs_mut().append_pair(&key, &value);
}
}

Response::builder()
.header("Content-Type", "text/html; charset=utf-8")
.header("Referrer-Policy", "strict-origin-when-cross-origin")
.header("Content-Security-Policy", "default-src 'none'")
.body(format!(
"<!doctype html><meta http-equiv=\"refresh\" content=\"0;url={}\">",
url.as_str().replace('&', "&amp;")
))
}
1 change: 1 addition & 0 deletions apps/app/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ fn main() {
toggle_decorations,
show_window,
restart_app,
api::youtube::youtube_embed_origin,
]);

tracing::info!("Initializing app...");
Expand Down
2 changes: 1 addition & 1 deletion apps/app/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost http://textures.minecraft.net blob: data:",
"style-src": "'unsafe-inline' 'self'",
"script-src": "https://js.stripe.com https://widget.intercom.io https://js.intercomcdn.com https://tally.so/widgets/embed.js 'self'",
"frame-src": "https://www.youtube.com https://www.youtube-nocookie.com https://discord.com https://tally.so/popup/ https://js.stripe.com https://hooks.stripe.com https://*.intercom.io https://intercom-sheets.com https://www.intercom-reporting.com https://app.intercom.com 'self'",
"frame-src": "http://127.0.0.1:* https://www.youtube.com https://www.youtube-nocookie.com https://discord.com https://tally.so/popup/ https://js.stripe.com https://hooks.stripe.com https://*.intercom.io https://intercom-sheets.com https://www.intercom-reporting.com https://app.intercom.com 'self'",
"media-src": "https://*.githubusercontent.com"
}
}
Expand Down
Loading