diff --git a/src/etapi/etapi_utils.ts b/src/etapi/etapi_utils.ts index 3ced5cc61..435de51d2 100644 --- a/src/etapi/etapi_utils.ts +++ b/src/etapi/etapi_utils.ts @@ -5,8 +5,8 @@ import becca from "../becca/becca.js"; import etapiTokenService from "../services/etapi_tokens.js"; import config from "../services/config.js"; import { NextFunction, Request, RequestHandler, Response, Router } from 'express'; -import { AppRequest, AppRequestHandler } from '../routes/route-interface.js'; import { ValidatorMap } from './etapi-interface.js'; +import { ApiRequestHandler } from "../routes/routes.js"; const GENERIC_CODE = "GENERIC"; type HttpMethod = "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head"; @@ -48,7 +48,7 @@ function checkEtapiAuth(req: Request, res: Response, next: NextFunction) { } } -function processRequest(req: Request, res: Response, routeHandler: AppRequestHandler, next: NextFunction, method: string, path: string) { +function processRequest(req: Request, res: Response, routeHandler: ApiRequestHandler, next: NextFunction, method: string, path: string) { try { cls.namespace.bindEmitter(req); cls.namespace.bindEmitter(res); @@ -57,7 +57,7 @@ function processRequest(req: Request, res: Response, routeHandler: AppRequestHan cls.set('componentId', "etapi"); cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']); - const cb = () => routeHandler(req as AppRequest, res, next); + const cb = () => routeHandler(req, res, next); return sql.transactional(cb); }); @@ -72,7 +72,7 @@ function processRequest(req: Request, res: Response, routeHandler: AppRequestHan } } -function route(router: Router, method: HttpMethod, path: string, routeHandler: AppRequestHandler) { +function route(router: Router, method: HttpMethod, path: string, routeHandler: ApiRequestHandler) { router[method](path, checkEtapiAuth, (req: Request, res: Response, next: NextFunction) => processRequest(req, res, routeHandler, next, method, path)); } diff --git a/src/etapi/notes.ts b/src/etapi/notes.ts index 846e77dc2..173dd0ec8 100644 --- a/src/etapi/notes.ts +++ b/src/etapi/notes.ts @@ -9,8 +9,7 @@ import searchService from "../services/search/services/search.js"; import SearchContext from "../services/search/search_context.js"; import zipExportService from "../services/export/zip.js"; import zipImportService from "../services/import/zip.js"; -import { Router } from 'express'; -import { AppRequest } from '../routes/route-interface.js'; +import { Request, Router } from 'express'; import { ParsedQs } from 'qs'; import { NoteParams } from '../services/note-interface.js'; import { SearchParams } from '../services/search/services/types.js'; @@ -192,7 +191,7 @@ function register(router: Router) { }); } -function parseSearchParams(req: AppRequest) { +function parseSearchParams(req: Request) { const rawSearchParams: SearchParams = { fastSearch: parseBoolean(req.query, 'fastSearch'), includeArchivedNotes: parseBoolean(req.query, 'includeArchivedNotes'), diff --git a/src/express.d.ts b/src/express.d.ts new file mode 100644 index 000000000..46f9521c4 --- /dev/null +++ b/src/express.d.ts @@ -0,0 +1,21 @@ +import { Session } from "express-session"; + +export declare module "express-serve-static-core" { + interface Request { + session: Session & { + loggedIn: boolean; + }, + headers: { + "x-local-date"?: string; + "x-labels"?: string; + + "authorization"?: string; + "trilium-cred"?: string; + "x-csrf-token"?: string; + + "trilium-component-id"?: string; + "trilium-local-now-datetime"?: string; + "trilium-hoisted-note-id"?: string; + } + } +} \ No newline at end of file diff --git a/src/routes/api/files.ts b/src/routes/api/files.ts index e05765ddb..753aeabf8 100644 --- a/src/routes/api/files.ts +++ b/src/routes/api/files.ts @@ -14,9 +14,8 @@ import ValidationError from "../../errors/validation_error.js"; import { Request, Response } from 'express'; import BNote from "../../becca/entities/bnote.js"; import BAttachment from "../../becca/entities/battachment.js"; -import { AppRequest } from '../route-interface.js'; -function updateFile(req: AppRequest) { +function updateFile(req: Request) { const note = becca.getNoteOrThrow(req.params.noteId); const file = req.file; @@ -43,7 +42,7 @@ function updateFile(req: AppRequest) { }; } -function updateAttachment(req: AppRequest) { +function updateAttachment(req: Request) { const attachment = becca.getAttachmentOrThrow(req.params.attachmentId); const file = req.file; if (!file) { diff --git a/src/routes/api/image.ts b/src/routes/api/image.ts index f6db84ca7..1d0bcb9c2 100644 --- a/src/routes/api/image.ts +++ b/src/routes/api/image.ts @@ -6,7 +6,6 @@ import fs from "fs"; import { Request, Response } from 'express'; import BNote from "../../becca/entities/bnote.js"; import BRevision from "../../becca/entities/brevision.js"; -import { AppRequest } from '../route-interface.js'; import { RESOURCE_DIR } from "../../services/resource_dir.js"; function returnImageFromNote(req: Request, res: Response) { @@ -82,7 +81,7 @@ function returnAttachedImage(req: Request, res: Response) { res.send(attachment.getContent()); } -function updateImage(req: AppRequest) { +function updateImage(req: Request) { const {noteId} = req.params; const {file} = req; diff --git a/src/routes/api/import.ts b/src/routes/api/import.ts index 1ea65d87b..d811cc045 100644 --- a/src/routes/api/import.ts +++ b/src/routes/api/import.ts @@ -13,9 +13,8 @@ import TaskContext from "../../services/task_context.js"; import ValidationError from "../../errors/validation_error.js"; import { Request } from 'express'; import BNote from "../../becca/entities/bnote.js"; -import { AppRequest } from '../route-interface.js'; -async function importNotesToBranch(req: AppRequest) { +async function importNotesToBranch(req: Request) { const { parentNoteId } = req.params; const { taskId, last } = req.body; @@ -97,7 +96,7 @@ async function importNotesToBranch(req: AppRequest) { return note.getPojo(); } -async function importAttachmentsToNote(req: AppRequest) { +async function importAttachmentsToNote(req: Request) { const { parentNoteId } = req.params; const { taskId, last } = req.body; diff --git a/src/routes/api/login.ts b/src/routes/api/login.ts index f5b813d2b..23b029bce 100644 --- a/src/routes/api/login.ts +++ b/src/routes/api/login.ts @@ -13,9 +13,8 @@ import sql from "../../services/sql.js"; import ws from "../../services/ws.js"; import etapiTokenService from "../../services/etapi_tokens.js"; import { Request } from 'express'; -import { AppRequest } from '../route-interface.js'; -function loginSync(req: AppRequest) { +function loginSync(req: Request) { if (!sqlInit.schemaExists()) { return [500, { message: "DB schema does not exist, can't sync." }]; } diff --git a/src/routes/api/sender.ts b/src/routes/api/sender.ts index e19086f56..890eefa98 100644 --- a/src/routes/api/sender.ts +++ b/src/routes/api/sender.ts @@ -6,9 +6,8 @@ import noteService from "../../services/notes.js"; import sanitize_attribute_name from "../../services/sanitize_attribute_name.js"; import specialNotesService from "../../services/special_notes.js"; import { Request } from 'express'; -import { AppRequest } from '../route-interface.js'; -function uploadImage(req: AppRequest) { +function uploadImage(req: Request) { const file = req.file; if (!file) { diff --git a/src/routes/login.ts b/src/routes/login.ts index aae82e042..4af0abda3 100644 --- a/src/routes/login.ts +++ b/src/routes/login.ts @@ -9,7 +9,6 @@ import assetPath from "../services/asset_path.js"; import appPath from "../services/app_path.js"; import ValidationError from "../errors/validation_error.js"; import { Request, Response } from 'express'; -import { AppRequest } from './route-interface.js'; function loginPage(req: Request, res: Response) { res.render('login', { @@ -57,7 +56,7 @@ function setPassword(req: Request, res: Response) { res.redirect('login'); } -function login(req: AppRequest, res: Response) { +function login(req: Request, res: Response) { const guessedPassword = req.body.password; if (verifyPassword(guessedPassword)) { @@ -93,7 +92,7 @@ function verifyPassword(guessedPassword: string) { return guess_hashed.equals(hashed_password); } -function logout(req: AppRequest, res: Response) { +function logout(req: Request, res: Response) { req.session.regenerate(() => { req.session.loggedIn = false; diff --git a/src/routes/route-interface.ts b/src/routes/route-interface.ts deleted file mode 100644 index 4510c5de6..000000000 --- a/src/routes/route-interface.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { NextFunction, Request, Response } from "express"; -import { Session, SessionData } from "express-session"; - -export interface AppRequest extends Request { - headers: { - authorization?: string; - "trilium-cred"?: string; - "x-local-date"?: string; - "x-labels"?: string; - "trilium-local-now-datetime"?: string; - } - session: Session & Partial & { - loggedIn: boolean; - } -} - -export type AppRequestHandler = ( - req: AppRequest, - res: Response, - next: NextFunction -) => void; \ No newline at end of file diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 52faa1b57..bb4fa7ca3 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -70,7 +70,6 @@ import etapiNoteRoutes from "../etapi/notes.js"; import etapiSpecialNoteRoutes from "../etapi/special_notes.js"; import etapiSpecRoute from "../etapi/spec.js"; import etapiBackupRoute from "../etapi/backup.js"; -import { AppRequest, AppRequestHandler } from './route-interface.js'; const csrfMiddleware = csurf({ cookie: { @@ -81,7 +80,8 @@ const csrfMiddleware = csurf({ const MAX_ALLOWED_FILE_SIZE_MB = 250; const GET = 'get', PST = 'post', PUT = 'put', PATCH = 'patch', DEL = 'delete'; -type ApiResultHandler = (req: express.Request, res: express.Response, result: unknown) => number; +export type ApiResultHandler = (req: express.Request, res: express.Response, result: unknown) => number; +export type ApiRequestHandler = (req: express.Request, res: express.Response, next: express.NextFunction) => unknown; // TODO: Deduplicate with etapi_utils.ts afterwards. type HttpMethod = "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head"; @@ -438,11 +438,11 @@ function send(res: express.Response, statusCode: number, response: unknown) { } } -function apiRoute(method: HttpMethod, path: string, routeHandler: express.Handler) { +function apiRoute(method: HttpMethod, path: string, routeHandler: ApiRequestHandler) { route(method, path, [auth.checkApiAuth, csrfMiddleware], routeHandler, apiResultHandler); } -function route(method: HttpMethod, path: string, middleware: (express.Handler | AppRequestHandler)[], routeHandler: AppRequestHandler, resultHandler: ApiResultHandler | null = null, transactional = true) { +function route(method: HttpMethod, path: string, middleware: express.Handler[], routeHandler: ApiRequestHandler, resultHandler: ApiResultHandler | null = null, transactional = true) { router[method](path, ...(middleware as express.Handler[]), (req: express.Request, res: express.Response, next: express.NextFunction) => { const start = Date.now(); @@ -455,7 +455,7 @@ function route(method: HttpMethod, path: string, middleware: (express.Handler | cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']); cls.set('hoistedNoteId', req.headers['trilium-hoisted-note-id'] || 'root'); - const cb = () => routeHandler(req as AppRequest, res, next); + const cb = () => routeHandler(req, res, next); return transactional ? sql.transactional(cb) : cb(); }); diff --git a/src/services/auth.ts b/src/services/auth.ts index f19976616..b7f183fc4 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -8,11 +8,10 @@ import passwordEncryptionService from "./encryption/password_encryption.js"; import config from "./config.js"; import passwordService from "./encryption/password.js"; import type { NextFunction, Request, Response } from 'express'; -import { AppRequest } from '../routes/route-interface.js'; const noAuthentication = config.General && config.General.noAuthentication === true; -function checkAuth(req: AppRequest, res: Response, next: NextFunction) { +function checkAuth(req: Request, res: Response, next: NextFunction) { if (!sqlInit.isDbInitialized()) { res.redirect("setup"); } @@ -26,7 +25,7 @@ function checkAuth(req: AppRequest, res: Response, next: NextFunction) { // for electron things which need network stuff // currently, we're doing that for file upload because handling form data seems to be difficult -function checkApiAuthOrElectron(req: AppRequest, res: Response, next: NextFunction) { +function checkApiAuthOrElectron(req: Request, res: Response, next: NextFunction) { if (!req.session.loggedIn && !utils.isElectron() && !noAuthentication) { reject(req, res, "Logged in session not found"); } @@ -35,7 +34,7 @@ function checkApiAuthOrElectron(req: AppRequest, res: Response, next: NextFuncti } } -function checkApiAuth(req: AppRequest, res: Response, next: NextFunction) { +function checkApiAuth(req: Request, res: Response, next: NextFunction) { if (!req.session.loggedIn && !noAuthentication) { reject(req, res, "Logged in session not found"); } @@ -44,7 +43,7 @@ function checkApiAuth(req: AppRequest, res: Response, next: NextFunction) { } } -function checkAppInitialized(req: AppRequest, res: Response, next: NextFunction) { +function checkAppInitialized(req: Request, res: Response, next: NextFunction) { if (!sqlInit.isDbInitialized()) { res.redirect("setup"); } @@ -53,7 +52,7 @@ function checkAppInitialized(req: AppRequest, res: Response, next: NextFunction) } } -function checkPasswordSet(req: AppRequest, res: Response, next: NextFunction) { +function checkPasswordSet(req: Request, res: Response, next: NextFunction) { if (!utils.isElectron() && !passwordService.isPasswordSet()) { res.redirect("set-password"); } else { @@ -61,7 +60,7 @@ function checkPasswordSet(req: AppRequest, res: Response, next: NextFunction) { } } -function checkPasswordNotSet(req: AppRequest, res: Response, next: NextFunction) { +function checkPasswordNotSet(req: Request, res: Response, next: NextFunction) { if (!utils.isElectron() && passwordService.isPasswordSet()) { res.redirect("login"); } else { @@ -69,7 +68,7 @@ function checkPasswordNotSet(req: AppRequest, res: Response, next: NextFunction) } } -function checkAppNotInitialized(req: AppRequest, res: Response, next: NextFunction) { +function checkAppNotInitialized(req: Request, res: Response, next: NextFunction) { if (sqlInit.isDbInitialized()) { reject(req, res, "App already initialized."); } @@ -78,7 +77,7 @@ function checkAppNotInitialized(req: AppRequest, res: Response, next: NextFuncti } } -function checkEtapiToken(req: AppRequest, res: Response, next: NextFunction) { +function checkEtapiToken(req: Request, res: Response, next: NextFunction) { if (etapiTokenService.isValidAuthHeader(req.headers.authorization)) { next(); } @@ -87,7 +86,7 @@ function checkEtapiToken(req: AppRequest, res: Response, next: NextFunction) { } } -function reject(req: AppRequest, res: Response, message: string) { +function reject(req: Request, res: Response, message: string) { log.info(`${req.method} ${req.path} rejected with 401 ${message}`); res.setHeader("Content-Type", "text/plain") @@ -95,7 +94,7 @@ function reject(req: AppRequest, res: Response, message: string) { .send(message); } -function checkCredentials(req: AppRequest, res: Response, next: NextFunction) { +function checkCredentials(req: Request, res: Response, next: NextFunction) { if (!sqlInit.isDbInitialized()) { res.setHeader("Content-Type", "text/plain") .status(400) @@ -111,6 +110,13 @@ function checkCredentials(req: AppRequest, res: Response, next: NextFunction) { } const header = req.headers['trilium-cred'] || ''; + if (typeof header !== "string") { + res.setHeader("Content-Type", "text/plain") + .status(400) + .send('Invalid data type for trilium-cred.'); + return; + } + const auth = Buffer.from(header, 'base64').toString(); const colonIndex = auth.indexOf(':'); const password = colonIndex === -1 ? "" : auth.substr(colonIndex + 1); diff --git a/src/share/routes.ts b/src/share/routes.ts index d1f3f4e9b..da90625ed 100644 --- a/src/share/routes.ts +++ b/src/share/routes.ts @@ -209,8 +209,9 @@ function register(router: Router) { shacaLoader.ensureLoad(); if (!shaca.shareRootNote) { - return res.status(404) + res.status(404) .json({ message: "Share root note not found" }); + return; } renderNote(shaca.shareRootNote, req, res); @@ -282,7 +283,7 @@ function register(router: Router) { } else if (image.type === "mindMap") { renderImageAttachment(image, res, 'mindmap-export.svg'); } else { - return res.status(400) + res.status(400) .json({ message: "Requested note is not a shareable image" }); } }); @@ -302,7 +303,7 @@ function register(router: Router) { addNoIndexHeader(attachment.note, res); res.send(attachment.getContent()); } else { - return res.status(400) + res.status(400) .json({ message: "Requested attachment is not a shareable image" }); } }); @@ -354,7 +355,8 @@ function register(router: Router) { let note; if (typeof ancestorNoteId !== "string") { - return res.status(400).json({ message: "'ancestorNoteId' parameter is mandatory." }); + res.status(400).json({ message: "'ancestorNoteId' parameter is mandatory." }); + return; } // This will automatically return if no ancestorNoteId is provided and there is no shareIndex @@ -365,7 +367,8 @@ function register(router: Router) { const { search } = req.query; if (typeof search !== "string" || !search?.trim()) { - return res.status(400).json({ message: "'search' parameter is mandatory." }); + res.status(400).json({ message: "'search' parameter is mandatory." }); + return; } const searchContext = new SearchContext({ ancestorNoteId: ancestorNoteId });