optional basic auth for shared notes, closes #2781

This commit is contained in:
zadam 2022-07-31 21:45:32 +02:00
parent 3ebfaec1bc
commit 46deceedc9
6 changed files with 263 additions and 506 deletions

657
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -70,6 +70,7 @@
"react-dom": "18.2.0",
"request": "2.88.2",
"rimraf": "3.0.2",
"safe-compare": "1.1.4",
"sanitize-filename": "1.6.3",
"sanitize-html": "2.7.1",
"sax": "1.2.4",

View File

@ -218,6 +218,7 @@ const ATTR_HELP = {
"shareRoot": "marks note which is served on /share root.",
"shareRaw": "note will be served in its raw format, without HTML wrapper",
"shareDisallowRobotIndexing": `will forbid robot indexing of this note via <code>X-Robots-Tag: noindex</code> header`,
"shareCredentials": "require credentials to access this shared note. Value is expected to be in format 'username:password'. Don't forget to make this inheritable to apply to child-notes/images.",
"displayRelations": "comma delimited names of relations which should be displayed. All other ones will be hidden.",
"hideRelations": "comma delimited names of relations which should be hidden. All other ones will be displayed.",
"titleTemplate": `default title of notes created as children of this note. The value is evaluated as JavaScript string

View File

@ -49,6 +49,7 @@ module.exports = [
{ type: 'label', name: 'shareRoot' },
{ type: 'label', name: 'shareRaw' },
{ type: 'label', name: 'shareDisallowRobotIndexing' },
{ type: 'label', name: 'shareCredentials' },
{ type: 'label', name: 'displayRelations' },
{ type: 'label', name: 'hideRelations' },
{ type: 'label', name: 'titleTemplate', isDangerous: true },

View File

@ -1,5 +1,6 @@
const express = require('express');
const path = require('path');
const safeCompare = require('safe-compare');
const shaca = require("./shaca/shaca");
const shacaLoader = require("./shaca/shaca_loader");
@ -29,13 +30,59 @@ function addNoIndexHeader(note, res) {
}
}
function reject(res) {
res.setHeader('WWW-Authenticate', 'Basic realm="User Visible Realm", charset="UTF-8"')
.sendStatus(401);
}
function checkNoteAccess(noteId, req, res) {
const note = shaca.getNote(noteId);
if (!note) {
res.setHeader("Content-Type", "text/plain")
.status(404)
.send(`Note '${noteId}' not found`);
return false;
}
const credentials = note.getCredentials();
if (credentials.length === 0) {
return note;
}
const header = req.header("Authorization");
if (!header?.startsWith("Basic ")) {
reject(res);
return false;
}
const base64Str = header.substring("Basic ".length);
const buffer = Buffer.from(base64Str, 'base64');
const authString = buffer.toString('utf-8');
for (const credentialLabel of credentials) {
if (safeCompare(authString, credentialLabel.value)) {
return note; // success;
}
}
return false;
}
function register(router) {
function renderNote(note, res) {
function renderNote(note, req, res) {
if (!note) {
res.status(404).render("share/404");
return;
}
if (!checkNoteAccess(note.noteId, req, res)) {
return;
}
addNoIndexHeader(note, res);
if (note.hasLabel('shareRaw') || ['image', 'file'].includes(note.type)) {
@ -63,7 +110,7 @@ function register(router) {
router.get(['/share', '/share/'], (req, res, next) => {
shacaLoader.ensureLoad();
renderNote(shaca.shareRootNote, res);
renderNote(shaca.shareRootNote, req, res);
});
router.get('/share/:shareId', (req, res, next) => {
@ -73,19 +120,15 @@ function register(router) {
const note = shaca.aliasToNote[shareId] || shaca.notes[shareId];
renderNote(note, res);
renderNote(note, req, res);
});
router.get('/share/api/notes/:noteId', (req, res, next) => {
shacaLoader.ensureLoad();
let note;
const {noteId} = req.params;
const note = shaca.getNote(noteId);
if (!note) {
return res.setHeader("Content-Type", "text/plain")
.status(404)
.send(`Note '${noteId}' not found`);
if (!(note = checkNoteAccess(req.params.noteId, req, res))) {
return;
}
addNoIndexHeader(note, res);
@ -96,13 +139,10 @@ function register(router) {
router.get('/share/api/notes/:noteId/download', (req, res, next) => {
shacaLoader.ensureLoad();
const {noteId} = req.params;
const note = shaca.getNote(noteId);
let note;
if (!note) {
return res.setHeader("Content-Type", "text/plain")
.status(404)
.send(`Note '${noteId}' not found`);
if (!(note = checkNoteAccess(req.params.noteId, req, res))) {
return;
}
addNoIndexHeader(note, res);
@ -123,14 +163,13 @@ function register(router) {
router.get('/share/api/images/:noteId/:filename', (req, res, next) => {
shacaLoader.ensureLoad();
const image = shaca.getNote(req.params.noteId);
let image;
if (!image) {
return res.setHeader('Content-Type', 'text/plain')
.status(404)
.send(`Note '${req.params.noteId}' not found`);
if (!(image = checkNoteAccess(req.params.noteId, req, res))) {
return;
}
else if (!["image", "canvas"].includes(image.type)) {
if (!["image", "canvas"].includes(image.type)) {
return res.setHeader('Content-Type', 'text/plain')
.status(400)
.send("Requested note is not a shareable image");
@ -165,13 +204,10 @@ function register(router) {
router.get('/share/api/notes/:noteId/view', (req, res, next) => {
shacaLoader.ensureLoad();
const {noteId} = req.params;
const note = shaca.getNote(noteId);
let note;
if (!note) {
return res.setHeader('Content-Type', 'text/plain')
.status(404)
.send(`Note '${noteId}' not found`);
if (!(note = checkNoteAccess(req.params.noteId, req, res))) {
return;
}
addNoIndexHeader(note, res);

View File

@ -6,6 +6,9 @@ const AbstractEntity = require('./abstract_entity');
const LABEL = 'label';
const RELATION = 'relation';
const CREDENTIALS = 'shareCredentials';
const isCredentials = attr => attr.type === 'label' && attr.name === CREDENTIALS;
class Note extends AbstractEntity {
constructor([noteId, title, type, mime, utcDateModified]) {
@ -115,19 +118,25 @@ class Note extends AbstractEntity {
this.__getAttributes([]);
if (type && name) {
return this.__attributeCache.filter(attr => attr.type === type && attr.name === name);
return this.__attributeCache.filter(attr => attr.type === type && attr.name === name && !isCredentials(attr));
}
else if (type) {
return this.__attributeCache.filter(attr => attr.type === type);
return this.__attributeCache.filter(attr => attr.type === type && !isCredentials(attr));
}
else if (name) {
return this.__attributeCache.filter(attr => attr.name === name);
return this.__attributeCache.filter(attr => attr.name === name && !isCredentials(attr));
}
else {
return this.__attributeCache.slice();
return this.__attributeCache.filter(attr => !isCredentials(attr));
}
}
getCredentials() {
this.__getAttributes([]);
return this.__attributeCache.filter(isCredentials);
}
__getAttributes(path) {
if (path.includes(this.noteId)) {
return [];