mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-03-30 13:45:19 +00:00
### Fixed
- No longer broken JSON files including `\n`, during the bucket synchronisation. (#623) - Custom headers and JWT tokens are now correctly sent to the server during configuration checking. (#624) ### Improved - Bucket synchronisation has been enhanced for better performance and reliability. - Now less duplicated chunks are sent to the server. - Fetching conflicted files from the server is now more reliable. - Dependent libraries have been updated to the latest version.
This commit is contained in:
@@ -1,16 +1,7 @@
|
||||
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
|
||||
import { LOG_LEVEL_DEBUG, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
|
||||
import { Notice, requestUrl, type RequestUrlParam, type RequestUrlResponse } from "../../deps.ts";
|
||||
import {
|
||||
type CouchDBCredentials,
|
||||
type EntryDoc,
|
||||
type FilePathWithPrefix,
|
||||
type JWTCredentials,
|
||||
type JWTHeader,
|
||||
type JWTParams,
|
||||
type JWTPayload,
|
||||
type PreparedJWT,
|
||||
} from "../../lib/src/common/types.ts";
|
||||
import { type CouchDBCredentials, type EntryDoc, type FilePathWithPrefix } from "../../lib/src/common/types.ts";
|
||||
import { getPathFromTFile } from "../../common/utils.ts";
|
||||
import {
|
||||
disableEncryption,
|
||||
@@ -22,9 +13,7 @@ import {
|
||||
import { setNoticeClass } from "../../lib/src/mock_and_interop/wrapper.ts";
|
||||
import { ObsHttpHandler } from "./APILib/ObsHttpHandler.ts";
|
||||
import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser.ts";
|
||||
import { reactive, reactiveSource } from "octagonal-wheels/dataobject/reactive.js";
|
||||
import { arrayBufferToBase64Single, writeString } from "../../lib/src/string_and_binary/convert.ts";
|
||||
import { Refiner } from "octagonal-wheels/dataobject/Refiner";
|
||||
import { AuthorizationHeaderGenerator } from "../../lib/src/replication/httplib.ts";
|
||||
|
||||
setNoticeClass(Notice);
|
||||
|
||||
@@ -44,10 +33,8 @@ async function fetchByAPI(request: RequestUrlParam): Promise<RequestUrlResponse>
|
||||
|
||||
export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidianModule {
|
||||
_customHandler!: ObsHttpHandler;
|
||||
authHeaderSource = reactiveSource<string>("");
|
||||
authHeader = reactive(() =>
|
||||
this.authHeaderSource.value == "" ? "" : "Basic " + window.btoa(this.authHeaderSource.value)
|
||||
);
|
||||
|
||||
_authHeader = new AuthorizationHeaderGenerator();
|
||||
|
||||
last_successful_post = false;
|
||||
$$customFetchHandler(): ObsHttpHandler {
|
||||
@@ -110,135 +97,6 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
|
||||
}
|
||||
}
|
||||
|
||||
_importKey(auth: JWTCredentials) {
|
||||
if (auth.jwtAlgorithm == "HS256" || auth.jwtAlgorithm == "HS512") {
|
||||
const key = (auth.jwtKey || "").trim();
|
||||
if (key == "") {
|
||||
throw new Error("JWT key is empty");
|
||||
}
|
||||
const binaryDerString = window.atob(key);
|
||||
const binaryDer = new Uint8Array(binaryDerString.length);
|
||||
for (let i = 0; i < binaryDerString.length; i++) {
|
||||
binaryDer[i] = binaryDerString.charCodeAt(i);
|
||||
}
|
||||
const hashName = auth.jwtAlgorithm == "HS256" ? "SHA-256" : "SHA-512";
|
||||
return crypto.subtle.importKey("raw", binaryDer, { name: "HMAC", hash: { name: hashName } }, true, [
|
||||
"sign",
|
||||
]);
|
||||
} else if (auth.jwtAlgorithm == "ES256" || auth.jwtAlgorithm == "ES512") {
|
||||
const pem = auth.jwtKey
|
||||
.replace(/-----BEGIN [^-]+-----/, "")
|
||||
.replace(/-----END [^-]+-----/, "")
|
||||
.replace(/\s+/g, "");
|
||||
// const pem = key.replace(/\s/g, "");
|
||||
const binaryDerString = window.atob(pem);
|
||||
const binaryDer = new Uint8Array(binaryDerString.length);
|
||||
for (let i = 0; i < binaryDerString.length; i++) {
|
||||
binaryDer[i] = binaryDerString.charCodeAt(i);
|
||||
}
|
||||
// const binaryDer = base64ToArrayBuffer(pem);
|
||||
const namedCurve = auth.jwtAlgorithm == "ES256" ? "P-256" : "P-521";
|
||||
const param = { name: "ECDSA", namedCurve };
|
||||
return crypto.subtle.importKey("pkcs8", binaryDer, param, true, ["sign"]);
|
||||
} else {
|
||||
throw new Error("Supplied JWT algorithm is not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
_currentCryptoKey = new Refiner<JWTCredentials, CryptoKey>({
|
||||
evaluation: async (auth, previous) => {
|
||||
return await this._importKey(auth);
|
||||
},
|
||||
});
|
||||
|
||||
_jwt = new Refiner<JWTParams, PreparedJWT>({
|
||||
evaluation: async (params, previous) => {
|
||||
const encodedHeader = btoa(JSON.stringify(params.header));
|
||||
const encodedPayload = btoa(JSON.stringify(params.payload));
|
||||
const buff = `${encodedHeader}.${encodedPayload}`.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
||||
|
||||
const key = await this._currentCryptoKey.update(params.credentials).value;
|
||||
let token = "";
|
||||
if (params.header.alg == "ES256" || params.header.alg == "ES512") {
|
||||
const jwt = await crypto.subtle.sign(
|
||||
{ name: "ECDSA", hash: { name: "SHA-256" } },
|
||||
key,
|
||||
writeString(buff)
|
||||
);
|
||||
token = (await arrayBufferToBase64Single(jwt))
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=/g, "");
|
||||
} else if (params.header.alg == "HS256" || params.header.alg == "HS512") {
|
||||
const jwt = await crypto.subtle.sign(
|
||||
{ name: "HMAC", hash: { name: params.header.alg } },
|
||||
key,
|
||||
writeString(buff)
|
||||
);
|
||||
token = (await arrayBufferToBase64Single(jwt))
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=/g, "");
|
||||
} else {
|
||||
throw new Error("JWT algorithm is not supported.");
|
||||
}
|
||||
return {
|
||||
...params,
|
||||
token: `${buff}.${token}`,
|
||||
} as PreparedJWT;
|
||||
},
|
||||
});
|
||||
|
||||
_jwtParams = new Refiner<JWTCredentials, JWTParams>({
|
||||
evaluation(source, previous) {
|
||||
const kid = source.jwtKid || undefined;
|
||||
const sub = (source.jwtSub || "").trim();
|
||||
if (sub == "") {
|
||||
throw new Error("JWT sub is empty");
|
||||
}
|
||||
const algorithm = source.jwtAlgorithm || "";
|
||||
if (!algorithm) {
|
||||
throw new Error("JWT algorithm is not configured.");
|
||||
}
|
||||
if (algorithm != "HS256" && algorithm != "HS512" && algorithm != "ES256" && algorithm != "ES512") {
|
||||
throw new Error("JWT algorithm is not supported.");
|
||||
}
|
||||
const header: JWTHeader = {
|
||||
alg: source.jwtAlgorithm || "HS256",
|
||||
typ: "JWT",
|
||||
kid,
|
||||
};
|
||||
const iat = ~~(new Date().getTime() / 1000);
|
||||
const exp = iat + (source.jwtExpDuration || 5) * 60; // 5 minutes
|
||||
const payload = {
|
||||
exp,
|
||||
iat,
|
||||
sub: source.jwtSub || "",
|
||||
"_couchdb.roles": ["_admin"],
|
||||
} satisfies JWTPayload;
|
||||
return {
|
||||
header,
|
||||
payload,
|
||||
credentials: source,
|
||||
};
|
||||
},
|
||||
shouldUpdate(isDifferent, source, previous) {
|
||||
if (isDifferent) {
|
||||
return true;
|
||||
}
|
||||
if (!previous) {
|
||||
return true;
|
||||
}
|
||||
// if expired.
|
||||
const d = ~~(new Date().getTime() / 1000);
|
||||
if (previous.payload.exp < d) {
|
||||
// console.warn(`jwt expired ${previous.payload.exp} < ${d}`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
async $$connectRemoteCouchDB(
|
||||
uri: string,
|
||||
auth: CouchDBCredentials,
|
||||
@@ -253,25 +111,14 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
|
||||
if (!isValidRemoteCouchDBURI(uri)) return "Remote URI is not valid";
|
||||
if (uri.toLowerCase() != uri) return "Remote URI and database name could not contain capital letters.";
|
||||
if (uri.indexOf(" ") !== -1) return "Remote URI and database name could not contain spaces.";
|
||||
let authHeader = "";
|
||||
if ("username" in auth) {
|
||||
const userNameAndPassword = auth.username && auth.password ? `${auth.username}:${auth.password}` : "";
|
||||
if (this.authHeaderSource.value != userNameAndPassword) {
|
||||
this.authHeaderSource.value = userNameAndPassword;
|
||||
}
|
||||
authHeader = this.authHeader.value;
|
||||
} else if ("jwtAlgorithm" in auth) {
|
||||
const params = await this._jwtParams.update(auth).value;
|
||||
const jwt = await this._jwt.update(params).value;
|
||||
const token = jwt.token;
|
||||
authHeader = `Bearer ${token}`;
|
||||
}
|
||||
// let authHeader = await this._authHeader.getAuthorizationHeader(auth);
|
||||
|
||||
const conf: PouchDB.HttpAdapter.HttpAdapterConfiguration = {
|
||||
adapter: "http",
|
||||
auth: "username" in auth ? auth : undefined,
|
||||
skip_setup: !performSetup,
|
||||
fetch: async (url: string | Request, opts?: RequestInit) => {
|
||||
const authHeader = await this._authHeader.getAuthorizationHeader(auth);
|
||||
let size = "";
|
||||
const localURL = url.toString().substring(uri.length);
|
||||
const method = opts?.method ?? "GET";
|
||||
@@ -288,27 +135,27 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
|
||||
size = ` (${opts_length})`;
|
||||
}
|
||||
try {
|
||||
const headers = new Headers(opts?.headers);
|
||||
if (customHeaders) {
|
||||
for (const [key, value] of Object.entries(customHeaders)) {
|
||||
if (key && value) {
|
||||
headers.append(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!("username" in auth)) {
|
||||
headers.append("authorization", authHeader);
|
||||
}
|
||||
|
||||
if (!disableRequestURI && typeof url == "string" && typeof (opts?.body ?? "") == "string") {
|
||||
return await this.fetchByAPI(url, localURL, method, authHeader, opts);
|
||||
// Deprecated configuration, only for backward compatibility.
|
||||
return await this.fetchByAPI(url, localURL, method, authHeader, { ...opts, headers });
|
||||
}
|
||||
// --> native Fetch API.
|
||||
|
||||
try {
|
||||
if (customHeaders) {
|
||||
for (const [key, value] of Object.entries(customHeaders)) {
|
||||
if (key && value) {
|
||||
(opts!.headers as Headers).append(key, value);
|
||||
}
|
||||
}
|
||||
// // Issue #407
|
||||
// (opts!.headers as Headers).append("ngrok-skip-browser-warning", "123");
|
||||
}
|
||||
// debugger;
|
||||
if (!("username" in auth)) {
|
||||
(opts!.headers as Headers).append("authorization", authHeader);
|
||||
}
|
||||
this.plugin.requestCount.value = this.plugin.requestCount.value + 1;
|
||||
const response: Response = await fetch(url, opts);
|
||||
const response: Response = await fetch(url, { ...opts, headers });
|
||||
if (method == "POST" || method == "PUT") {
|
||||
this.last_successful_post = response.ok;
|
||||
} else {
|
||||
@@ -344,7 +191,10 @@ export class ModuleObsidianAPI extends AbstractObsidianModule implements IObsidi
|
||||
this._log(
|
||||
"Failed to fetch by native fetch API. Trying to fetch by API to get more information."
|
||||
);
|
||||
const resp2 = await this.fetchByAPI(url.toString(), localURL, method, authHeader, opts);
|
||||
const resp2 = await this.fetchByAPI(url.toString(), localURL, method, authHeader, {
|
||||
...opts,
|
||||
headers,
|
||||
});
|
||||
if (resp2.status / 100 == 2) {
|
||||
this._log(
|
||||
"The request was successful by API. But the native fetch API failed! Please check CORS settings on the remote database!. While this condition, you cannot enable LiveSync",
|
||||
|
||||
Reference in New Issue
Block a user