mirror of
https://github.com/LBRYFoundation/Watch-on-LBRY.git
synced 2025-08-23 17:47:26 +00:00
🍙 auth added
This commit is contained in:
parent
3e60ed295f
commit
365173d316
9 changed files with 106 additions and 31 deletions
56
src/common/crypto.ts
Normal file
56
src/common/crypto.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
export async function generateKeys() {
|
||||||
|
const keys = await window.crypto.subtle.generateKey(
|
||||||
|
{
|
||||||
|
name: "RSASSA-PKCS1-v1_5",
|
||||||
|
// Consider using a 4096-bit key for systems that require long-term security
|
||||||
|
modulusLength: 2048,
|
||||||
|
publicExponent: new Uint8Array([1, 0, 1]),
|
||||||
|
hash: "SHA-256",
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
["sign", "verify"]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
publicKey: await exportPublicKey(keys.publicKey),
|
||||||
|
privateKey: await exportPrivateKey(keys.privateKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportPrivateKey(key: CryptoKey) {
|
||||||
|
const exported = await window.crypto.subtle.exportKey(
|
||||||
|
"pkcs8",
|
||||||
|
key
|
||||||
|
)
|
||||||
|
return Buffer.from(exported).toString('base64')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportPublicKey(key: CryptoKey) {
|
||||||
|
const exported = await window.crypto.subtle.exportKey(
|
||||||
|
"spki",
|
||||||
|
key
|
||||||
|
)
|
||||||
|
return Buffer.from(exported).toString('base64')
|
||||||
|
}
|
||||||
|
|
||||||
|
function importPrivateKey(base64: string) {
|
||||||
|
|
||||||
|
return window.crypto.subtle.importKey(
|
||||||
|
"pkcs8",
|
||||||
|
Buffer.from(base64, 'base64'),
|
||||||
|
{
|
||||||
|
name: "RSASSA-PKCS1-v1_5",
|
||||||
|
hash: "SHA-256",
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
["sign"]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sign(data: string, privateKey: string) {
|
||||||
|
return Buffer.from(await window.crypto.subtle.sign(
|
||||||
|
{ name: "RSASSA-PKCS1-v1_5" },
|
||||||
|
await importPrivateKey(privateKey),
|
||||||
|
Buffer.from(data)
|
||||||
|
))
|
||||||
|
}
|
|
@ -4,16 +4,22 @@ export interface ExtensionSettings {
|
||||||
redirect: boolean
|
redirect: boolean
|
||||||
targetPlatform: TargetPlatformName
|
targetPlatform: TargetPlatformName
|
||||||
urlResolver: YTUrlResolverName
|
urlResolver: YTUrlResolverName
|
||||||
|
publicKey?: string,
|
||||||
|
privateKey?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: ExtensionSettings = { redirect: true, targetPlatform: 'odysee', urlResolver: 'lbryInc' }
|
|
||||||
|
|
||||||
|
export const DEFAULT_SETTINGS: ExtensionSettings = {
|
||||||
|
redirect: true,
|
||||||
|
targetPlatform: 'odysee',
|
||||||
|
urlResolver: 'odyseeApi'
|
||||||
|
}
|
||||||
|
|
||||||
export function getExtensionSettingsAsync(): Promise<ExtensionSettings> {
|
export function getExtensionSettingsAsync(): Promise<ExtensionSettings> {
|
||||||
return new Promise(resolve => chrome.storage.local.get(o => resolve(o as any)))
|
return new Promise(resolve => chrome.storage.local.get(o => resolve(o as any)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const targetPlatform = (o: {
|
const targetPlatform = (o: {
|
||||||
domainPrefix: string
|
domainPrefix: string
|
||||||
displayName: string
|
displayName: string
|
||||||
|
@ -69,8 +75,6 @@ export const targetPlatformSettings = {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const sourcePlatform = (o: {
|
const sourcePlatform = (o: {
|
||||||
hostnames: string[]
|
hostnames: string[]
|
||||||
htmlQueries: {
|
htmlQueries: {
|
||||||
|
@ -111,12 +115,16 @@ export type YTUrlResolver = ReturnType<typeof ytUrlResolver>
|
||||||
export type YTUrlResolverName = Extract<keyof typeof ytUrlResolversSettings, string>
|
export type YTUrlResolverName = Extract<keyof typeof ytUrlResolversSettings, string>
|
||||||
export const getYtUrlResolversSettingsEntiries = () => Object.entries(ytUrlResolversSettings) as any as [Extract<keyof typeof ytUrlResolversSettings, string>, YTUrlResolver][]
|
export const getYtUrlResolversSettingsEntiries = () => Object.entries(ytUrlResolversSettings) as any as [Extract<keyof typeof ytUrlResolversSettings, string>, YTUrlResolver][]
|
||||||
export const ytUrlResolversSettings = {
|
export const ytUrlResolversSettings = {
|
||||||
lbryInc: ytUrlResolver({
|
odyseeApi: ytUrlResolver({
|
||||||
name: "Odysee",
|
name: "Odysee",
|
||||||
href: "https://api.odysee.com/yt/resolve"
|
href: "https://api.odysee.com/yt/resolve"
|
||||||
}),
|
}),
|
||||||
madiatorFinder: ytUrlResolver({
|
madiatorFinder: ytUrlResolver({
|
||||||
name: "Madiator Finder",
|
name: "Madiator Finder",
|
||||||
href: "https://finder.madiator.com/api/v1/resolve"
|
href: "https://finder.madiator.com/api/v1/resolve"
|
||||||
|
}),
|
||||||
|
local: ytUrlResolver({
|
||||||
|
name: "Local",
|
||||||
|
href: "http://localhost:3000/api/v1/resolve"
|
||||||
})
|
})
|
||||||
}
|
}
|
|
@ -1,13 +1,14 @@
|
||||||
import { useEffect, useReducer } from 'preact/hooks'
|
import { useEffect, useReducer } from 'preact/hooks'
|
||||||
import { DEFAULT_SETTINGS } from './settings'
|
import { generateKeys } from './crypto'
|
||||||
|
import { DEFAULT_SETTINGS, ExtensionSettings } from './settings'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A hook to read the settings from local storage
|
* A hook to read the settings from local storage
|
||||||
*
|
*
|
||||||
* @param initial the default value. Must have all relevant keys present and should not change
|
* @param initial the default value. Must have all relevant keys present and should not change
|
||||||
*/
|
*/
|
||||||
export function useSettings<T extends object>(initial: T) {
|
export function useSettings(initial: ExtensionSettings) {
|
||||||
const [state, dispatch] = useReducer((state, nstate: Partial<T>) => ({ ...state, ...nstate }), initial)
|
const [state, dispatch] = useReducer((state, nstate: Partial<ExtensionSettings>) => ({ ...state, ...nstate }), initial)
|
||||||
// register change listeners, gets current values, and cleans up the listeners on unload
|
// register change listeners, gets current values, and cleans up the listeners on unload
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const changeListener = (changes: Record<string, chrome.storage.StorageChange>, areaName: string) => {
|
const changeListener = (changes: Record<string, chrome.storage.StorageChange>, areaName: string) => {
|
||||||
|
@ -19,12 +20,22 @@ export function useSettings<T extends object>(initial: T) {
|
||||||
dispatch(Object.fromEntries(changeSet))
|
dispatch(Object.fromEntries(changeSet))
|
||||||
}
|
}
|
||||||
chrome.storage.onChanged.addListener(changeListener)
|
chrome.storage.onChanged.addListener(changeListener)
|
||||||
chrome.storage.local.get(Object.keys(initial), o => dispatch(o as Partial<T>))
|
chrome.storage.local.get(Object.keys(initial), o => dispatch(o as Partial<ExtensionSettings>))
|
||||||
|
|
||||||
|
generateKeys().then((keys) => {
|
||||||
|
setSetting('publicKey', keys.publicKey)
|
||||||
|
setSetting('privateKey', keys.privateKey)
|
||||||
|
})
|
||||||
|
|
||||||
return () => chrome.storage.onChanged.removeListener(changeListener)
|
return () => chrome.storage.onChanged.removeListener(changeListener)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Utilty to set a setting in the browser */
|
||||||
|
export const setSetting = <K extends keyof ExtensionSettings>(setting: K, value: ExtensionSettings[K]) => chrome.storage.local.set({ [setting]: value })
|
||||||
|
|
||||||
|
|
||||||
/** A hook to read watch on lbry settings from local storage */
|
/** A hook to read watch on lbry settings from local storage */
|
||||||
export const useLbrySettings = () => useSettings(DEFAULT_SETTINGS)
|
export const useLbrySettings = () => useSettings(DEFAULT_SETTINGS)
|
|
@ -37,8 +37,7 @@ async function clearExpired() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clearAll()
|
async function clearAll() {
|
||||||
{
|
|
||||||
return await new Promise<void>((resolve, reject) => {
|
return await new Promise<void>((resolve, reject) => {
|
||||||
const store = db?.transaction("store", "readwrite").objectStore("store")
|
const store = db?.transaction("store", "readwrite").objectStore("store")
|
||||||
if (!store) return resolve()
|
if (!store) return resolve()
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { chunk } from "lodash"
|
import { chunk } from "lodash"
|
||||||
|
import { sign } from "../crypto"
|
||||||
import { getExtensionSettingsAsync, ytUrlResolversSettings } from "../settings"
|
import { getExtensionSettingsAsync, ytUrlResolversSettings } from "../settings"
|
||||||
import { LbryPathnameCache } from "./urlCache"
|
import { LbryPathnameCache } from "./urlCache"
|
||||||
|
|
||||||
// const LBRY_API_HOST = 'https://api.odysee.com'; MOVED TO SETTINGS
|
|
||||||
const QUERY_CHUNK_SIZE = 100
|
const QUERY_CHUNK_SIZE = 100
|
||||||
|
|
||||||
|
|
||||||
export type YtUrlResolveItem = { type: 'video' | 'channel', id: string }
|
export type YtUrlResolveItem = { type: 'video' | 'channel', id: string }
|
||||||
type Results = Record<string, YtUrlResolveItem>
|
type Results = Record<string, YtUrlResolveItem>
|
||||||
type Paramaters = YtUrlResolveItem[]
|
type Paramaters = YtUrlResolveItem[]
|
||||||
|
@ -16,7 +15,7 @@ interface ApiResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveById(params: Paramaters, progressCallback?: (progress: number) => void): Promise<Results> {
|
export async function resolveById(params: Paramaters, progressCallback?: (progress: number) => void): Promise<Results> {
|
||||||
const { urlResolver: urlResolverSettingName } = await getExtensionSettingsAsync()
|
const { urlResolver: urlResolverSettingName, privateKey, publicKey } = await getExtensionSettingsAsync()
|
||||||
const urlResolverSetting = ytUrlResolversSettings[urlResolverSettingName]
|
const urlResolverSetting = ytUrlResolversSettings[urlResolverSettingName]
|
||||||
|
|
||||||
async function requestChunk(params: Paramaters) {
|
async function requestChunk(params: Paramaters) {
|
||||||
|
@ -43,6 +42,11 @@ export async function resolveById(params: Paramaters, progressCallback?: (progre
|
||||||
const url = new URL(`${urlResolverSetting.href}`)
|
const url = new URL(`${urlResolverSetting.href}`)
|
||||||
url.searchParams.set('video_ids', params.filter((item) => item.type === 'video').map((item) => item.id).join(','))
|
url.searchParams.set('video_ids', params.filter((item) => item.type === 'video').map((item) => item.id).join(','))
|
||||||
url.searchParams.set('channel_ids', params.filter((item) => item.type === 'channel').map((item) => item.id).join(','))
|
url.searchParams.set('channel_ids', params.filter((item) => item.type === 'channel').map((item) => item.id).join(','))
|
||||||
|
if (publicKey && privateKey)
|
||||||
|
url.searchParams.set('keys', JSON.stringify({
|
||||||
|
signature: await sign(url.searchParams.toString(), privateKey),
|
||||||
|
publicKey
|
||||||
|
}))
|
||||||
|
|
||||||
const apiResponse = await fetch(url.toString(), { cache: 'no-store' })
|
const apiResponse = await fetch(url.toString(), { cache: 'no-store' })
|
||||||
if (apiResponse.ok) {
|
if (apiResponse.ok) {
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
"https://odysee.com/",
|
"https://odysee.com/",
|
||||||
"https://madiator.com/",
|
"https://madiator.com/",
|
||||||
"https://finder.madiator.com/",
|
"https://finder.madiator.com/",
|
||||||
|
"http://localhost:3000",
|
||||||
"tabs",
|
"tabs",
|
||||||
"storage"
|
"storage"
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import { h, render } from 'preact'
|
import { h, render } from 'preact'
|
||||||
import { useState } from 'preact/hooks'
|
import { useState } from 'preact/hooks'
|
||||||
import ButtonRadio, { SelectionOption } from '../common/components/ButtonRadio'
|
import ButtonRadio, { SelectionOption } from '../common/components/ButtonRadio'
|
||||||
import { ExtensionSettings, getTargetPlatfromSettingsEntiries, getYtUrlResolversSettingsEntiries, TargetPlatformName, YTUrlResolverName } from '../common/settings'
|
import { getTargetPlatfromSettingsEntiries, getYtUrlResolversSettingsEntiries, TargetPlatformName, YTUrlResolverName } from '../common/settings'
|
||||||
import { useLbrySettings } from '../common/useSettings'
|
import { setSetting, useLbrySettings } from '../common/useSettings'
|
||||||
import { LbryPathnameCache } from '../common/yt/urlCache'
|
import { LbryPathnameCache } from '../common/yt/urlCache'
|
||||||
import './popup.sass'
|
import './popup.sass'
|
||||||
|
|
||||||
/** Utilty to set a setting in the browser */
|
|
||||||
const setSetting = <K extends keyof ExtensionSettings>(setting: K, value: ExtensionSettings[K]) => chrome.storage.local.set({ [setting]: value })
|
|
||||||
|
|
||||||
/** Gets all the options for redirect destinations as selection options */
|
/** Gets all the options for redirect destinations as selection options */
|
||||||
const platformOptions: SelectionOption[] = getTargetPlatfromSettingsEntiries()
|
const platformOptions: SelectionOption[] = getTargetPlatfromSettingsEntiries()
|
||||||
|
@ -21,6 +19,7 @@ function WatchOnLbryPopup() {
|
||||||
let [clearingCache, updateClearingCache] = useState(() => false)
|
let [clearingCache, updateClearingCache] = useState(() => false)
|
||||||
|
|
||||||
return <div className='container'>
|
return <div className='container'>
|
||||||
|
{ }
|
||||||
<section>
|
<section>
|
||||||
<label className='radio-label'>Enable Redirection:</label>
|
<label className='radio-label'>Enable Redirection:</label>
|
||||||
<ButtonRadio value={redirect ? 'YES' : 'NO'} options={['YES', 'NO']}
|
<ButtonRadio value={redirect ? 'YES' : 'NO'} options={['YES', 'NO']}
|
||||||
|
|
|
@ -25,8 +25,7 @@ async function lbryPathnameFromVideoId(videoId: string): Promise<string | null>
|
||||||
}
|
}
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener(({ videoId }: { videoId: string }, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener(({ videoId }: { videoId: string }, sender, sendResponse) => {
|
||||||
lbryPathnameFromVideoId(videoId).then((lbryPathname) => sendResponse(lbryPathname)).catch((err) =>
|
lbryPathnameFromVideoId(videoId).then((lbryPathname) => sendResponse(lbryPathname)).catch((err) => {
|
||||||
{
|
|
||||||
sendResponse('error')
|
sendResponse('error')
|
||||||
console.error(err)
|
console.error(err)
|
||||||
})
|
})
|
||||||
|
|
|
@ -131,12 +131,10 @@ async function requestLbryPathname(videoId: string) {
|
||||||
// On redirect with app, people might choose to cancel browser's dialog
|
// On redirect with app, people might choose to cancel browser's dialog
|
||||||
// So we dont destroy the current window automatically for them
|
// So we dont destroy the current window automatically for them
|
||||||
// And also we are keeping the same window for less distiraction
|
// And also we are keeping the same window for less distiraction
|
||||||
if (settings.redirect)
|
if (settings.redirect) {
|
||||||
{
|
|
||||||
location.replace(url.toString())
|
location.replace(url.toString())
|
||||||
}
|
}
|
||||||
else
|
else {
|
||||||
{
|
|
||||||
open(url.toString(), '_blank')
|
open(url.toString(), '_blank')
|
||||||
if (window.history.length === 1) window.close()
|
if (window.history.length === 1) window.close()
|
||||||
else window.history.back()
|
else window.history.back()
|
||||||
|
|
Loading…
Add table
Reference in a new issue