mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-08 16:27:31 +00:00
feat: add timezone and encrypt cookies endpoint
This commit is contained in:
parent
949519aa61
commit
4129a61d85
@ -197,19 +197,22 @@ class Spotube extends HookConsumerWidget {
|
||||
"http://${server.server.address.host}:${server.port}",
|
||||
serverSecret: ref.read(serverRandomSecretProvider),
|
||||
pluginScript: """
|
||||
console.log("Local Timezone", Timezone.getLocalTimezone());
|
||||
console.log("Available Timezones", Timezone.getAvailableTimezones());
|
||||
class AuthEndpoint {
|
||||
}
|
||||
class CoreEndpoint {
|
||||
async checkUpdate() {
|
||||
console.log(globalThis);
|
||||
// const webview = await WebView.create("https://spotube.krtirtho.dev");
|
||||
// webview.onUrlChange((url) => {
|
||||
// console.log("url_request: ", url);
|
||||
// if (url.includes("/about")) {
|
||||
// webview.close();
|
||||
// }
|
||||
// });
|
||||
// await webview.open();
|
||||
const webview = await WebView.create("https://spotube.krtirtho.dev");
|
||||
webview.onUrlChange(async (url) => {
|
||||
console.log("url_request: ", url);
|
||||
if (url.includes("/about")) {
|
||||
console.log(await webview.cookies())
|
||||
webview.close();
|
||||
}
|
||||
});
|
||||
await webview.open();
|
||||
// const res = await SpotubeForm.show("Hello", [
|
||||
// {
|
||||
// objectType: "input",
|
||||
@ -222,9 +225,8 @@ class CoreEndpoint {
|
||||
// }
|
||||
// ])
|
||||
// console.log("Form Result: ", res);
|
||||
|
||||
console.log("LocalStorage Value: ", localStorage.getItem("test_key"));
|
||||
localStorage.setItem("test_key", "test_value");
|
||||
// console.log("LocalStorage Value: ", localStorage.getItem("test_key"));
|
||||
// localStorage.setItem("test_key", "test_value");
|
||||
}
|
||||
}
|
||||
class Plugin {
|
||||
|
||||
@ -1,16 +1,38 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
import 'package:spotube/provider/server/server.dart';
|
||||
import 'package:spotube/src/plugin_api/webview/webview.dart';
|
||||
import 'package:async/async.dart';
|
||||
import 'package:encrypt/encrypt.dart' as encrypt;
|
||||
|
||||
class ServerWebviewRoutes {
|
||||
final Ref ref;
|
||||
ServerWebviewRoutes({required this.ref});
|
||||
|
||||
final Map<String, Webview> _webviews = {};
|
||||
|
||||
String _encryptCookies(dynamic cookies, String secret) {
|
||||
final keyBytes = base64.decode(secret);
|
||||
final key = encrypt.Key(keyBytes);
|
||||
final ivBytes = List<int>.generate(16, (_) => Random.secure().nextInt(256));
|
||||
final iv = encrypt.IV(Uint8List.fromList(ivBytes));
|
||||
|
||||
final encrypter = encrypt.Encrypter(
|
||||
encrypt.AES(key, mode: encrypt.AESMode.cbc, padding: 'PKCS7'),
|
||||
);
|
||||
|
||||
final encrypted = encrypter.encrypt(jsonEncode(cookies), iv: iv);
|
||||
final combined = iv.bytes + encrypted.bytes;
|
||||
return base64.encode(combined);
|
||||
}
|
||||
|
||||
Future<Response> postCreateWebview(Request request) async {
|
||||
final payload = jsonDecode(await request.readAsString());
|
||||
final uri = Uri.parse(payload['url'] as String);
|
||||
@ -105,6 +127,8 @@ class ServerWebviewRoutes {
|
||||
}
|
||||
|
||||
Future<Response> postGetWebviewCookies(Request request) async {
|
||||
final secret = ref.read(serverRandomSecretProvider);
|
||||
|
||||
final body = jsonDecode(await request.readAsString());
|
||||
final uid = body['uid'] as String;
|
||||
final url = body['url'] as String;
|
||||
@ -114,8 +138,9 @@ class ServerWebviewRoutes {
|
||||
return Response.notFound('Webview with uid $uid not found');
|
||||
}
|
||||
final cookies = await webview.getCookies(url);
|
||||
final encryptedCookies = _encryptCookies(cookies, secret);
|
||||
return Response.ok(
|
||||
jsonEncode(cookies),
|
||||
jsonEncode({'data': encryptedCookies}),
|
||||
encoding: utf8,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -131,4 +156,5 @@ class ServerWebviewRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
final serverWebviewRoutesProvider = Provider((ref) => ServerWebviewRoutes());
|
||||
final serverWebviewRoutesProvider =
|
||||
Provider((ref) => ServerWebviewRoutes(ref: ref));
|
||||
|
||||
@ -15,7 +15,7 @@ final serverRandomSecretProvider = Provider<String>(
|
||||
(ref) {
|
||||
final random = Random.secure();
|
||||
final values = List<int>.generate(16, (i) => random.nextInt(256));
|
||||
return base64Url.encode(values);
|
||||
return base64.encode(values);
|
||||
},
|
||||
);
|
||||
final serverProvider = FutureProvider(
|
||||
|
||||
40
rust/Cargo.lock
generated
40
rust/Cargo.lock
generated
@ -364,6 +364,16 @@ dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono-tz"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"phf 0.12.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.5.0-rc.2"
|
||||
@ -1812,7 +1822,7 @@ dependencies = [
|
||||
"hex-simd",
|
||||
"llrt_build",
|
||||
"memchr",
|
||||
"phf",
|
||||
"phf 0.13.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2456,6 +2466,15 @@ version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7"
|
||||
dependencies = [
|
||||
"phf_shared 0.12.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.13.1"
|
||||
@ -2463,7 +2482,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
|
||||
dependencies = [
|
||||
"phf_macros",
|
||||
"phf_shared",
|
||||
"phf_shared 0.13.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2473,7 +2492,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"phf_shared",
|
||||
"phf_shared 0.13.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2483,12 +2502,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
"phf_shared 0.13.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.13.1"
|
||||
@ -2922,11 +2950,15 @@ name = "rust_lib_spotube"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"chrono-tz",
|
||||
"confy",
|
||||
"eventsource-client",
|
||||
"flutter_rust_bridge",
|
||||
"heck",
|
||||
"iana-time-zone",
|
||||
"llrt_modules",
|
||||
"openssl",
|
||||
"reqwest",
|
||||
"rquickjs",
|
||||
"serde",
|
||||
|
||||
@ -22,6 +22,10 @@ llrt_modules = { git = "https://github.com/awslabs/llrt.git", rev = "7d749dd18cf
|
||||
eventsource-client = "0.15.1"
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
confy = "2.0.0"
|
||||
chrono-tz = "0.10"
|
||||
iana-time-zone = "0.1"
|
||||
base64 = "0.22.1"
|
||||
openssl = "0.10.75"
|
||||
|
||||
[patch."https://github.com/DelSkayn/rquickjs"]
|
||||
rquickjs = "0.10.0"
|
||||
|
||||
@ -12,7 +12,7 @@ use crate::api::plugin::senders::{
|
||||
};
|
||||
use crate::frb_generated::StreamSink;
|
||||
use crate::internal::apis;
|
||||
use crate::internal::apis::{form, get_platform_directories, webview};
|
||||
use crate::internal::apis::{form, get_platform_directories, timezone, webview};
|
||||
use anyhow::anyhow;
|
||||
use flutter_rust_bridge::{frb, Rust2DartSendError};
|
||||
use llrt_modules::module_builder::ModuleBuilder;
|
||||
@ -55,7 +55,8 @@ async fn create_context(
|
||||
.with_global(timers::init)
|
||||
.with_global(util::init)
|
||||
.with_global(form::init)
|
||||
.with_global(webview::init);
|
||||
.with_global(webview::init)
|
||||
.with_global(timezone::init);
|
||||
|
||||
let (module_resolver, module_loader, global_attachment) = module_builder.build();
|
||||
runtime
|
||||
|
||||
@ -4,10 +4,12 @@ use serde::{Deserialize, Serialize};
|
||||
pub mod form;
|
||||
pub mod local_storage;
|
||||
pub mod webview;
|
||||
pub mod timezone;
|
||||
|
||||
pub fn init(ctx: &Ctx, endpoint_url: String, secret: String) -> rquickjs::Result<()> {
|
||||
ctx.globals().set("__serverUrl", endpoint_url)?;
|
||||
ctx.globals().set("__serverSecret", secret)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
27
rust/src/internal/apis/timezone.rs
Normal file
27
rust/src/internal/apis/timezone.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use rquickjs::prelude::Func;
|
||||
use rquickjs::{Class, Ctx, Object};
|
||||
|
||||
pub fn get_local_timezone() -> rquickjs::Result<String> {
|
||||
let timezone = iana_time_zone::get_timezone()
|
||||
.map_err(|e| rquickjs::Error::new_from_js_message("Timezone", "Error", &e.to_string()))?;
|
||||
Ok(timezone)
|
||||
}
|
||||
|
||||
pub fn get_available_timezones() -> rquickjs::Result<Vec<String>> {
|
||||
let timezones: Vec<String> = chrono_tz::TZ_VARIANTS
|
||||
.iter()
|
||||
.map(|tz| tz.name().to_string())
|
||||
.collect();
|
||||
Ok(timezones)
|
||||
}
|
||||
|
||||
pub fn init(ctx: &Ctx) -> rquickjs::Result<()> {
|
||||
let globals = ctx.globals();
|
||||
let timezone_obj = Object::new(ctx.clone())?;
|
||||
timezone_obj.set("getLocalTimezone", Func::new(get_local_timezone))?;
|
||||
timezone_obj.set("getAvailableTimezones", Func::new(get_available_timezones))?;
|
||||
|
||||
globals.set("Timezone", timezone_obj)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
use eventsource_client::{Client as EventSourceClient, ClientBuilder};
|
||||
use flutter_rust_bridge::for_generated::futures::StreamExt;
|
||||
use rquickjs::{class::Trace, Class, Ctx, Function, JsLifetime};
|
||||
use openssl::symm::{decrypt, Cipher};
|
||||
use rquickjs::{class::Trace, CatchResultExt, Class, Ctx, Function, JsLifetime, Value};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
@ -78,7 +80,7 @@ impl<'js> WebView<'js> {
|
||||
Class::instance(ctx, webview)
|
||||
}
|
||||
|
||||
pub async fn open(&self) -> rquickjs::Result<()> {
|
||||
pub async fn open(&self, ctx: Ctx<'js>) -> rquickjs::Result<()> {
|
||||
let client = reqwest::Client::new();
|
||||
let endpoint = format!("{}/plugin-api/webview/open", self.endpoint_url);
|
||||
|
||||
@ -95,12 +97,13 @@ impl<'js> WebView<'js> {
|
||||
rquickjs::Error::new_from_js_message("reqwest", "Error", &e.to_string())
|
||||
})?;
|
||||
|
||||
self.url_change_task().await;
|
||||
self.url_change_task(ctx).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn cookies(&self, ctx: Ctx<'js>) -> rquickjs::Result<rquickjs::Value<'js>> {
|
||||
pub async fn cookies(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
|
||||
let secret: String = ctx.globals().get("__serverSecret").unwrap_or_default();
|
||||
let client = reqwest::Client::new();
|
||||
let endpoint = format!("{}/plugin-api/webview/cookies", self.endpoint_url);
|
||||
|
||||
@ -122,7 +125,36 @@ impl<'js> WebView<'js> {
|
||||
rquickjs::Error::new_from_js_message("reqwest", "Error", &e.to_string())
|
||||
})?;
|
||||
|
||||
let value = ctx.json_parse(data.to_string())?;
|
||||
let enc = data.get("data").and_then(|v| v.as_str()).ok_or_else(|| {
|
||||
rquickjs::Error::new_from_js_message("cookies", "Error", "missing encrypted data")
|
||||
})?;
|
||||
|
||||
let combined = STANDARD.decode(enc.trim()).map_err(|e| {
|
||||
rquickjs::Error::new_from_js_message("cookies", "Error", &format!("b64 decode: {}", e))
|
||||
})?;
|
||||
|
||||
if combined.len() < 16 {
|
||||
return Err(rquickjs::Error::new_from_js_message(
|
||||
"cookies",
|
||||
"Error",
|
||||
"invalid payload (too short)",
|
||||
));
|
||||
}
|
||||
|
||||
let (iv, cipher_bytes) = combined.split_at(16);
|
||||
let key = STANDARD.decode(secret.trim()).map_err(|e| {
|
||||
rquickjs::Error::new_from_js_message("cookies", "Error", &format!("key decode: {}", e))
|
||||
})?;
|
||||
|
||||
let plain = decrypt(Cipher::aes_128_cbc(), &key, Some(iv), cipher_bytes).map_err(|e| {
|
||||
rquickjs::Error::new_from_js_message("cookies", "Error", &format!("decrypt: {}", e))
|
||||
})?;
|
||||
|
||||
let cookies_json: serde_json::Value = serde_json::from_slice(&plain).map_err(|e| {
|
||||
rquickjs::Error::new_from_js_message("cookies", "Error", &format!("json decode: {}", e))
|
||||
})?;
|
||||
|
||||
let value = ctx.json_parse(cookies_json.to_string())?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
@ -152,7 +184,7 @@ impl<'js> WebView<'js> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn url_change_task(&self) {
|
||||
async fn url_change_task(&self, ctx: Ctx<'js>) {
|
||||
let endpoint = format!(
|
||||
"{}/plugin-api/webview/{}/on-url-request",
|
||||
self.endpoint_url, self.uid
|
||||
@ -181,8 +213,17 @@ impl<'js> WebView<'js> {
|
||||
{
|
||||
let url = data.get("url").cloned().unwrap_or_default();
|
||||
for callback in self.callbacks.iter() {
|
||||
if let Err(e) = callback.call::<_, ()>((url.clone(),)) {
|
||||
eprintln!("Error in onUrlChange callback: {}", e);
|
||||
match callback.call::<_, Value>((url.clone(),)) {
|
||||
Ok(res) => {
|
||||
if let Some(promise) = res.into_promise() {
|
||||
if let Err(e) = promise.into_future::<()>().await.catch(&ctx) {
|
||||
eprintln!("Error in onUrlChange promise: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error in onUrlChange callback: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -203,8 +244,6 @@ impl<'js> WebView<'js> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub fn init(ctx: &Ctx) -> rquickjs::Result<()> {
|
||||
Class::<WebView>::define(&ctx.globals())?;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user