mirror of
https://github.com/TeamPiped/Piped.git
synced 2024-08-14 23:57:27 +00:00
Merge a5c0d8007c
into a055ae6686
This commit is contained in:
commit
d26ce27691
9 changed files with 220 additions and 19 deletions
|
@ -21,6 +21,7 @@
|
||||||
"linkify-html": "4.1.1",
|
"linkify-html": "4.1.1",
|
||||||
"linkifyjs": "4.1.1",
|
"linkifyjs": "4.1.1",
|
||||||
"mux.js": "6.3.0",
|
"mux.js": "6.3.0",
|
||||||
|
"pako": "2.1.0",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
"shaka-player": "4.5.0",
|
"shaka-player": "4.5.0",
|
||||||
"stream-browserify": "3.0.0",
|
"stream-browserify": "3.0.0",
|
||||||
|
|
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
|
@ -38,6 +38,9 @@ dependencies:
|
||||||
mux.js:
|
mux.js:
|
||||||
specifier: 6.3.0
|
specifier: 6.3.0
|
||||||
version: 6.3.0
|
version: 6.3.0
|
||||||
|
pako:
|
||||||
|
specifier: 2.1.0
|
||||||
|
version: 2.1.0
|
||||||
qrcode:
|
qrcode:
|
||||||
specifier: ^1.5.3
|
specifier: ^1.5.3
|
||||||
version: 1.5.3
|
version: 1.5.3
|
||||||
|
@ -4194,6 +4197,10 @@ packages:
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/pako@2.1.0:
|
||||||
|
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/parent-module@1.0.1:
|
/parent-module@1.0.1:
|
||||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
65
src/App.vue
65
src/App.vue
|
@ -9,7 +9,7 @@
|
||||||
</router-view>
|
</router-view>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FooterComponent />
|
<FooterComponent :config="config" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -19,6 +19,10 @@ import FooterComponent from "./components/FooterComponent.vue";
|
||||||
|
|
||||||
const darkModePreference = window.matchMedia("(prefers-color-scheme: dark)");
|
const darkModePreference = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
|
||||||
|
import { generateKey, encodeArrayToBase64, decodeBase64ToArray, decryptAESGCM } from "./utils/encryptionUtils";
|
||||||
|
import { decompressGzip } from "./utils/compressionUtils";
|
||||||
|
import { state } from "./utils/store";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
NavBar,
|
NavBar,
|
||||||
|
@ -27,6 +31,7 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
|
config: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -35,6 +40,15 @@ export default {
|
||||||
this.setTheme();
|
this.setTheme();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.fetchJson(this.authApiUrl() + "/config")
|
||||||
|
.then(config => {
|
||||||
|
this.config = config;
|
||||||
|
state.config = config;
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.onConfigLoaded();
|
||||||
|
});
|
||||||
|
|
||||||
if ("indexedDB" in window) {
|
if ("indexedDB" in window) {
|
||||||
const request = indexedDB.open("piped-db", 5);
|
const request = indexedDB.open("piped-db", 5);
|
||||||
request.onupgradeneeded = ev => {
|
request.onupgradeneeded = ev => {
|
||||||
|
@ -115,6 +129,55 @@ export default {
|
||||||
const themeColor = document.querySelector("meta[name='theme-color']");
|
const themeColor = document.querySelector("meta[name='theme-color']");
|
||||||
themeColor.setAttribute("content", currentColor[this.theme]);
|
themeColor.setAttribute("content", currentColor[this.theme]);
|
||||||
},
|
},
|
||||||
|
async onConfigLoaded() {
|
||||||
|
if (this.config.s3Enabled && this.authenticated) {
|
||||||
|
if (this.getPreferenceBoolean("syncPreferences", false, false)) {
|
||||||
|
var e2e2_b64_key = this.getPreferenceString("e2ee_key", null, false);
|
||||||
|
if (!e2e2_b64_key) {
|
||||||
|
const key = new Uint8Array(await generateKey());
|
||||||
|
const encoded = encodeArrayToBase64(key);
|
||||||
|
this.setPreference("e2ee_key", encoded);
|
||||||
|
e2e2_b64_key = encoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
const statResult = await this.fetchJson(
|
||||||
|
this.authApiUrl() + "/storage/stat",
|
||||||
|
{
|
||||||
|
file: "pipedpref",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: this.getAuthToken(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (statResult.status === "exists") {
|
||||||
|
const data = await fetch(this.authApiUrl() + "/storage/get?file=pipedpref", {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: this.getAuthToken(),
|
||||||
|
},
|
||||||
|
}).then(resp => resp.arrayBuffer());
|
||||||
|
|
||||||
|
const cryptoKey = decodeBase64ToArray(e2e2_b64_key).buffer;
|
||||||
|
|
||||||
|
const decrypted = await decryptAESGCM(data, cryptoKey);
|
||||||
|
|
||||||
|
const decompressed = await decompressGzip(new Uint8Array(decrypted));
|
||||||
|
|
||||||
|
const localStorageJson = JSON.parse(decompressed);
|
||||||
|
|
||||||
|
// import into localStorage
|
||||||
|
for (var key in localStorageJson) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(localStorageJson, key)) {
|
||||||
|
localStorage[key] = localStorageJson[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -29,6 +29,12 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
props: {
|
||||||
|
config: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
donationHref: null,
|
donationHref: null,
|
||||||
|
@ -36,16 +42,13 @@ export default {
|
||||||
privacyPolicyHref: null,
|
privacyPolicyHref: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
watch: {
|
||||||
this.fetchConfig();
|
config: {
|
||||||
},
|
handler() {
|
||||||
methods: {
|
this.donationHref = this.config?.donationUrl;
|
||||||
async fetchConfig() {
|
this.statusPageHref = this.config?.statusPageUrl;
|
||||||
this.fetchJson(this.apiUrl() + "/config").then(config => {
|
this.privacyPolicyHref = this.config?.privacyPolicyUrl;
|
||||||
this.donationHref = config?.donationUrl;
|
},
|
||||||
this.statusPageHref = config?.statusPageUrl;
|
|
||||||
this.privacyPolicyHref = config?.privacyPolicyUrl;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -380,6 +380,9 @@
|
||||||
<script>
|
<script>
|
||||||
import CountryMap from "@/utils/CountryMaps/en.json";
|
import CountryMap from "@/utils/CountryMaps/en.json";
|
||||||
import ConfirmModal from "./ConfirmModal.vue";
|
import ConfirmModal from "./ConfirmModal.vue";
|
||||||
|
import { state } from "../utils/store";
|
||||||
|
import { encryptAESGCM, decodeBase64ToArray } from "../utils/encryptionUtils";
|
||||||
|
import { compressGzip } from "../utils/compressionUtils";
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
ConfirmModal,
|
ConfirmModal,
|
||||||
|
@ -614,6 +617,53 @@ export default {
|
||||||
localStorage.setItem("hideWatched", this.hideWatched);
|
localStorage.setItem("hideWatched", this.hideWatched);
|
||||||
localStorage.setItem("mobileChapterLayout", this.mobileChapterLayout);
|
localStorage.setItem("mobileChapterLayout", this.mobileChapterLayout);
|
||||||
|
|
||||||
|
const config = state.config;
|
||||||
|
|
||||||
|
const key = this.getPreferenceString("e2ee_key", null, false);
|
||||||
|
|
||||||
|
if (config.s3Enabled && this.authenticated && key) {
|
||||||
|
const statResult = await this.fetchJson(
|
||||||
|
this.authApiUrl() + "/storage/stat",
|
||||||
|
{
|
||||||
|
file: "pipedpref",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: this.getAuthToken(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const etag = statResult.etag;
|
||||||
|
|
||||||
|
// export localStorage to JSON
|
||||||
|
const localStorageJson = {};
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
const key = localStorage.key(i);
|
||||||
|
localStorageJson[key] = localStorage.getItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const importedKey = decodeBase64ToArray(key).buffer;
|
||||||
|
|
||||||
|
const data = await compressGzip(JSON.stringify(localStorageJson));
|
||||||
|
|
||||||
|
const encrypted = await encryptAESGCM(data, importedKey);
|
||||||
|
|
||||||
|
await this.fetchJson(
|
||||||
|
this.authApiUrl() + "/storage/put",
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: this.getAuthToken(),
|
||||||
|
"x-file-name": "pipedpref",
|
||||||
|
"x-last-etag": etag,
|
||||||
|
},
|
||||||
|
body: encrypted,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldReload) window.location.reload();
|
if (shouldReload) window.location.reload();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
16
src/main.js
16
src/main.js
|
@ -125,10 +125,10 @@ const mixin = {
|
||||||
if (!disableAlert) alert(this.$t("info.local_storage"));
|
if (!disableAlert) alert(this.$t("info.local_storage"));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getPreferenceBoolean(key, defaultVal) {
|
getPreferenceBoolean(key, defaultVal, allowQuery = true) {
|
||||||
var value;
|
var value;
|
||||||
if (
|
if (
|
||||||
(value = new URLSearchParams(window.location.search).get(key)) !== null ||
|
(allowQuery && (value = new URLSearchParams(window.location.search).get(key)) !== null) ||
|
||||||
(this.testLocalStorage && (value = localStorage.getItem(key)) !== null)
|
(this.testLocalStorage && (value = localStorage.getItem(key)) !== null)
|
||||||
) {
|
) {
|
||||||
switch (String(value).toLowerCase()) {
|
switch (String(value).toLowerCase()) {
|
||||||
|
@ -142,29 +142,29 @@ const mixin = {
|
||||||
}
|
}
|
||||||
} else return defaultVal;
|
} else return defaultVal;
|
||||||
},
|
},
|
||||||
getPreferenceString(key, defaultVal) {
|
getPreferenceString(key, defaultVal, allowQuery = true) {
|
||||||
var value;
|
var value;
|
||||||
if (
|
if (
|
||||||
(value = new URLSearchParams(window.location.search).get(key)) !== null ||
|
(allowQuery && (value = new URLSearchParams(window.location.search).get(key)) !== null) ||
|
||||||
(this.testLocalStorage && (value = localStorage.getItem(key)) !== null)
|
(this.testLocalStorage && (value = localStorage.getItem(key)) !== null)
|
||||||
) {
|
) {
|
||||||
return value;
|
return value;
|
||||||
} else return defaultVal;
|
} else return defaultVal;
|
||||||
},
|
},
|
||||||
getPreferenceNumber(key, defaultVal) {
|
getPreferenceNumber(key, defaultVal, allowQuery = true) {
|
||||||
var value;
|
var value;
|
||||||
if (
|
if (
|
||||||
(value = new URLSearchParams(window.location.search).get(key)) !== null ||
|
(allowQuery && (value = new URLSearchParams(window.location.search).get(key)) !== null) ||
|
||||||
(this.testLocalStorage && (value = localStorage.getItem(key)) !== null)
|
(this.testLocalStorage && (value = localStorage.getItem(key)) !== null)
|
||||||
) {
|
) {
|
||||||
const num = Number(value);
|
const num = Number(value);
|
||||||
return isNaN(num) ? defaultVal : num;
|
return isNaN(num) ? defaultVal : num;
|
||||||
} else return defaultVal;
|
} else return defaultVal;
|
||||||
},
|
},
|
||||||
getPreferenceJSON(key, defaultVal) {
|
getPreferenceJSON(key, defaultVal, allowQuery = true) {
|
||||||
var value;
|
var value;
|
||||||
if (
|
if (
|
||||||
(value = new URLSearchParams(window.location.search).get(key)) !== null ||
|
(allowQuery && (value = new URLSearchParams(window.location.search).get(key)) !== null) ||
|
||||||
(this.testLocalStorage && (value = localStorage.getItem(key)) !== null)
|
(this.testLocalStorage && (value = localStorage.getItem(key)) !== null)
|
||||||
) {
|
) {
|
||||||
return JSON.parse(value);
|
return JSON.parse(value);
|
||||||
|
|
38
src/utils/compressionUtils.js
Normal file
38
src/utils/compressionUtils.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
export const compressGzip = async data => {
|
||||||
|
// Firefox does not support CompressionStream yet
|
||||||
|
if (typeof CompressionStream !== "undefined") {
|
||||||
|
let bytes = new TextEncoder().encode(data);
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
const cs = new CompressionStream("gzip");
|
||||||
|
const writer = cs.writable.getWriter();
|
||||||
|
writer.write(bytes);
|
||||||
|
writer.close();
|
||||||
|
const compAb = await new Response(cs.readable).arrayBuffer();
|
||||||
|
bytes = new Uint8Array(compAb);
|
||||||
|
|
||||||
|
return bytes;
|
||||||
|
} else {
|
||||||
|
const pako = await import("pako");
|
||||||
|
return pako.gzip(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decompressGzip = async compressedData => {
|
||||||
|
// Firefox does not support DecompressionStream yet
|
||||||
|
if (typeof DecompressionStream !== "undefined") {
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
const ds = new DecompressionStream("gzip");
|
||||||
|
const writer = ds.writable.getWriter();
|
||||||
|
writer.write(compressedData);
|
||||||
|
writer.close();
|
||||||
|
const decompAb = await new Response(ds.readable).arrayBuffer();
|
||||||
|
const bytes = new Uint8Array(decompAb);
|
||||||
|
|
||||||
|
return new TextDecoder().decode(bytes);
|
||||||
|
} else {
|
||||||
|
const pako = await import("pako");
|
||||||
|
const inflated = pako.inflate(compressedData, { to: "string" });
|
||||||
|
|
||||||
|
return inflated;
|
||||||
|
}
|
||||||
|
};
|
36
src/utils/encryptionUtils.js
Normal file
36
src/utils/encryptionUtils.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// These functions accept and return Uint8Arrays
|
||||||
|
|
||||||
|
export async function encryptAESGCM(plaintext, key) {
|
||||||
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||||
|
const algorithm = { name: "AES-GCM", iv: iv };
|
||||||
|
const keyMaterial = await crypto.subtle.importKey("raw", key, algorithm, false, ["encrypt"]);
|
||||||
|
const ciphertext = await crypto.subtle.encrypt(algorithm, keyMaterial, plaintext);
|
||||||
|
|
||||||
|
return new Uint8Array([...iv, ...new Uint8Array(ciphertext)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function decryptAESGCM(ciphertextArray, key) {
|
||||||
|
const iv = new Uint8Array(ciphertextArray.slice(0, 12));
|
||||||
|
const algorithm = { name: "AES-GCM", iv: iv };
|
||||||
|
const keyMaterial = await crypto.subtle.importKey("raw", key, algorithm, false, ["decrypt"]);
|
||||||
|
const decrypted = await crypto.subtle.decrypt(algorithm, keyMaterial, new Uint8Array(ciphertextArray.slice(12)));
|
||||||
|
|
||||||
|
return decrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateKey() {
|
||||||
|
const algorithm = { name: "AES-GCM", length: 256 };
|
||||||
|
const key = await crypto.subtle.generateKey(algorithm, true, ["encrypt", "decrypt"]);
|
||||||
|
|
||||||
|
return await crypto.subtle.exportKey("raw", key);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeArrayToBase64(array) {
|
||||||
|
const chars = String.fromCharCode.apply(null, array);
|
||||||
|
return btoa(chars);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeBase64ToArray(base64) {
|
||||||
|
const chars = atob(base64);
|
||||||
|
return new Uint8Array(chars.split("").map(c => c.charCodeAt(0)));
|
||||||
|
}
|
3
src/utils/store.js
Normal file
3
src/utils/store.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { reactive } from "vue";
|
||||||
|
|
||||||
|
export const state = reactive({});
|
Loading…
Add table
Add a link
Reference in a new issue