-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Fix share access to attachments for notes protected by login:password #7828
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,6 +40,13 @@ interface Subroot { | |
|
|
||
| type GetNoteFunction = (id: string) => SNote | BNote | null; | ||
|
|
||
| function addContentAccessQuery(note: SNote | BNote, secondEl?:boolean) { | ||
| if (!(note instanceof BNote) && note.contentAccessor && note.contentAccessor?.type === "query") { | ||
| return secondEl ? `&cat=${note.contentAccessor.getToken()}` : `?cat=${note.contentAccessor.getToken()}`; | ||
| } | ||
| return "" | ||
| } | ||
|
Comment on lines
+43
to
+48
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why have three different mechanisms in place for content accessors? This creates a maintenance burden that I don't think is justified. |
||
|
|
||
| function getSharedSubTreeRoot(note: SNote | BNote | undefined): Subroot { | ||
| if (!note || note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) { | ||
| // share root itself is not shared | ||
|
|
@@ -111,19 +118,19 @@ export function renderNoteContent(note: SNote) { | |
| cssToLoad.push(`assets/scripts.css`); | ||
| } | ||
| for (const cssRelation of note.getRelations("shareCss")) { | ||
| cssToLoad.push(`api/notes/${cssRelation.value}/download`); | ||
| cssToLoad.push(`api/notes/${cssRelation.value}/download${addContentAccessQuery(note)}`); | ||
| } | ||
|
|
||
| // Determine JS to load. | ||
| const jsToLoad: string[] = [ | ||
| "assets/scripts.js" | ||
| ]; | ||
| for (const jsRelation of note.getRelations("shareJs")) { | ||
| jsToLoad.push(`api/notes/${jsRelation.value}/download`); | ||
| jsToLoad.push(`api/notes/${jsRelation.value}/download${addContentAccessQuery(note)}`); | ||
| } | ||
|
|
||
| const customLogoId = note.getRelation("shareLogo")?.value; | ||
| const logoUrl = customLogoId ? `api/images/${customLogoId}/image.png` : `../${assetUrlFragment}/images/icon-color.svg`; | ||
| const logoUrl = customLogoId ? `api/images/${customLogoId}/image.png${addContentAccessQuery(note)}` : `../${assetUrlFragment}/images/icon-color.svg`; | ||
|
|
||
| return renderNoteContentInternal(note, { | ||
| subRoot, | ||
|
|
@@ -133,7 +140,7 @@ export function renderNoteContent(note: SNote) { | |
| logoUrl, | ||
| ancestors, | ||
| isStatic: false, | ||
| faviconUrl: note.hasRelation("shareFavicon") ? `api/notes/${note.getRelationValue("shareFavicon")}/download` : `../favicon.ico` | ||
| faviconUrl: note.hasRelation("shareFavicon") ? `api/notes/${note.getRelationValue("shareFavicon")}/download${addContentAccessQuery(note)}` : `../favicon.ico` | ||
| }); | ||
| } | ||
|
|
||
|
|
@@ -158,6 +165,7 @@ function renderNoteContentInternal(note: SNote | BNote, renderArgs: RenderArgs) | |
| isEmpty, | ||
| assetPath: shareAdjustedAssetPath, | ||
| assetUrlFragment, | ||
| addContentAccessQuery: (second: boolean | undefined) => addContentAccessQuery(note, second), | ||
| showLoginInShareTheme, | ||
| t, | ||
| isDev, | ||
|
|
@@ -325,7 +333,7 @@ function renderText(result: Result, note: SNote | BNote) { | |
| } | ||
|
|
||
| if (href?.startsWith("#")) { | ||
| handleAttachmentLink(linkEl, href, getNote, getAttachment); | ||
| handleAttachmentLink(linkEl, href, getNote, getAttachment, note); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -349,15 +357,15 @@ function renderText(result: Result, note: SNote | BNote) { | |
| } | ||
| } | ||
|
|
||
| function handleAttachmentLink(linkEl: HTMLElement, href: string, getNote: GetNoteFunction, getAttachment: (id: string) => BAttachment | SAttachment | null) { | ||
| function handleAttachmentLink(linkEl: HTMLElement, href: string, getNote: GetNoteFunction, getAttachment: (id: string) => BAttachment | SAttachment | null, note: SNote | BNote) { | ||
| const linkRegExp = /attachmentId=([a-zA-Z0-9_]+)/g; | ||
| let attachmentMatch; | ||
| if ((attachmentMatch = linkRegExp.exec(href))) { | ||
| const attachmentId = attachmentMatch[1]; | ||
| const attachment = getAttachment(attachmentId); | ||
|
|
||
| if (attachment) { | ||
| linkEl.setAttribute("href", `api/attachments/${attachmentId}/download`); | ||
| linkEl.setAttribute("href", `api/attachments/${attachmentId}/download${addContentAccessQuery(note)}`); | ||
| linkEl.classList.add(`attachment-link`); | ||
| linkEl.classList.add(`role-${attachment.role}`); | ||
| linkEl.childNodes.length = 0; | ||
|
|
@@ -430,7 +438,7 @@ function renderMermaid(result: Result, note: SNote | BNote) { | |
| } | ||
|
|
||
| result.content = ` | ||
| <img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}"> | ||
| <img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}${addContentAccessQuery(note, true)}"> | ||
| <hr> | ||
| <details> | ||
| <summary>Chart source</summary> | ||
|
|
@@ -439,14 +447,14 @@ function renderMermaid(result: Result, note: SNote | BNote) { | |
| } | ||
|
|
||
| function renderImage(result: Result, note: SNote | BNote) { | ||
| result.content = `<img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}">`; | ||
| result.content = `<img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}${addContentAccessQuery(note, true)}">`; | ||
| } | ||
|
|
||
| function renderFile(note: SNote | BNote, result: Result) { | ||
| if (note.mime === "application/pdf") { | ||
| result.content = `<iframe class="pdf-view" src="api/notes/${note.noteId}/view"></iframe>`; | ||
| result.content = `<iframe class="pdf-view" src="api/notes/${note.noteId}/view${addContentAccessQuery(note)}"></iframe>`; | ||
| } else { | ||
| result.content = `<button type="button" onclick="location.href='api/notes/${note.noteId}/download'">Download file</button>`; | ||
| result.content = `<button type="button" onclick="location.href='api/notes/${note.noteId}/download${addContentAccessQuery(note)}'">Download file</button>`; | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| import crypto from "crypto"; | ||
| import SNote from "./snote"; | ||
| import utils from "../../../services/utils"; | ||
|
|
||
| const DefaultAccessTimeoutSec = 10 * 60; // 10 minutes | ||
|
|
||
| export class ContentAccessor { | ||
| note: SNote; | ||
| token: string; | ||
| timestamp: number; | ||
| type: string; | ||
| timeout: number; | ||
| key: Buffer; | ||
|
|
||
| constructor(note: SNote) { | ||
| this.note = note; | ||
| this.key = crypto.randomBytes(32); | ||
| this.token = ""; | ||
| this.timestamp = 0; | ||
| this.timeout = Number(this.note.getAttributeValue("label", "shareAccessTokenTimeout") || DefaultAccessTimeoutSec) | ||
|
|
||
| switch (this.note.getAttributeValue("label", "shareContentAccess")) { | ||
| case "basic": this.type = "basic"; break | ||
| case "query": this.type = "query"; break | ||
| default: this.type = "cookie"; break | ||
| }; | ||
|
|
||
| } | ||
|
|
||
| __encrypt(text: string) { | ||
| const iv = crypto.randomBytes(16); | ||
| const cipher = crypto.createCipheriv('aes-256-cbc', this.key, iv); | ||
| let encrypted = cipher.update(text, 'utf8', 'hex'); | ||
| encrypted += cipher.final('hex'); | ||
| return iv.toString('hex') + encrypted; | ||
| } | ||
|
|
||
| __decrypt(encryptedText: string) { | ||
| try { | ||
| const iv = Buffer.from(encryptedText.slice(0, 32), 'hex'); | ||
| const decipher = crypto.createDecipheriv('aes-256-cbc', this.key, iv); | ||
| let decrypted = decipher.update(encryptedText.slice(32), 'hex', 'utf8'); | ||
| decrypted += decipher.final('utf8'); | ||
| return decrypted; | ||
| } catch { | ||
| return "" | ||
| } | ||
| } | ||
|
|
||
| __compare(originalText: string, encryptedText: string) { | ||
| return originalText === this.__decrypt(encryptedText) | ||
| } | ||
|
|
||
| update() { | ||
| if (new Date().getTime() < this.timestamp + this.getTimeout() * 1000) return | ||
| this.token = utils.randomString(36); | ||
| this.key = crypto.randomBytes(32); | ||
| this.timestamp = new Date().getTime(); | ||
| } | ||
|
|
||
| isTokenValid(encToken: string) { | ||
| return this.__compare(this.token, encToken) && new Date().getTime() < this.timestamp + this.getTimeout() * 1000; | ||
| } | ||
|
|
||
| getToken() { | ||
| return this.__encrypt(this.token); | ||
| } | ||
|
|
||
| getTokenExpiration() { | ||
| return (this.timestamp + (this.timeout * 1000) - new Date().getTime()) /1000; | ||
| } | ||
|
|
||
| getTimeout() { | ||
| return this.timeout; | ||
| } | ||
|
|
||
| getContentAccessType() { | ||
| return this.type; | ||
| } | ||
|
|
||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's enough to do
pnpm --filter=share-theme buildinstead ofpnpm run server:buildbecause that one takes a lot longer to execute.