mirror of
https://github.com/wekan/wekan.git
synced 2026-11-07 02:52:33 -05:00
136 lines
4.9 KiB
JavaScript
136 lines
4.9 KiB
JavaScript
// Who is making this plain HTTP request?
|
|
//
|
|
// `Meteor.userId()` CANNOT ANSWER THAT. It reads the current DDP invocation's
|
|
// environment, which exists inside a method or a publication and nowhere else. In
|
|
// a WebApp/connect handler there is no such environment, so it does not return
|
|
// "nobody" - it throws:
|
|
//
|
|
// Meteor.userId can only be invoked in method calls or publications.
|
|
//
|
|
// Two file routes called it anyway, and because both wrap their body in a
|
|
// try/catch that turns any error into a 500, the failure looked like a broken
|
|
// file rather than broken authentication:
|
|
//
|
|
// server/routes/avatarServer.js /cdn/storage/avatars/:fileName
|
|
// server/routes/legacyAttachments.js
|
|
//
|
|
// That is why profile pictures came back as broken images after upgrading from an
|
|
// old MongoDB snap: the stored `profile.avatarUrl` of a 6.x install is
|
|
// `/cfs/files/avatars/<id>`, the legacy route redirects it to
|
|
// `/cdn/storage/avatars/<id>`, and that route could never serve anybody. The
|
|
// avatar files themselves migrated fine - the request for them was answered 500
|
|
// before a byte was read.
|
|
//
|
|
// An HTTP request carries its identity in the request: a bearer token, an
|
|
// X-Auth-Token header, an ?authToken= parameter, or the login cookie. This is
|
|
// the same resolution server/routes/universalFileServer.js has always done -
|
|
// lifted here so the routes that were guessing can share it rather than grow a
|
|
// third copy.
|
|
|
|
import { Meteor } from 'meteor/meteor';
|
|
|
|
export function parseCookies(req) {
|
|
const header = req && req.headers && req.headers.cookie;
|
|
const out = {};
|
|
if (!header) return out;
|
|
for (const part of header.split(';')) {
|
|
const idx = part.indexOf('=');
|
|
if (idx === -1) continue;
|
|
try {
|
|
out[decodeURIComponent(part.slice(0, idx).trim())] =
|
|
decodeURIComponent(part.slice(idx + 1).trim());
|
|
} catch (_) {
|
|
// A cookie we cannot decode is not a reason to fail the request.
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function parseQuery(req) {
|
|
const out = {};
|
|
const q = ((req && req.url) || '').split('?')[1] || '';
|
|
if (!q) return out;
|
|
for (const pair of q.split('&')) {
|
|
if (!pair) continue;
|
|
const [rawK, rawV] = pair.split('=');
|
|
try {
|
|
out[decodeURIComponent((rawK || '').trim())] =
|
|
decodeURIComponent((rawV || '').trim());
|
|
} catch (_) {
|
|
// ditto
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** The login token this request carries, or null. Priority order matches
|
|
* universalFileServer's, so a request that works for an attachment works for an
|
|
* avatar. */
|
|
export function extractLoginToken(req) {
|
|
const authz = req && req.headers &&
|
|
(req.headers.authorization || req.headers.Authorization);
|
|
if (authz && typeof authz === 'string') {
|
|
const m = authz.match(/^Bearer\s+(.+)$/i);
|
|
if (m && m[1]) return m[1].trim();
|
|
}
|
|
|
|
const xAuth = req && req.headers &&
|
|
(req.headers['x-auth-token'] || req.headers['X-Auth-Token']);
|
|
if (xAuth && typeof xAuth === 'string') return xAuth.trim();
|
|
|
|
const q = parseQuery(req);
|
|
if (q.authToken && typeof q.authToken === 'string') return q.authToken.trim();
|
|
|
|
const cookies = parseCookies(req);
|
|
if (cookies.meteor_login_token) return cookies.meteor_login_token.trim();
|
|
if (cookies.wekan_login_token) return cookies.wekan_login_token.trim();
|
|
|
|
return null;
|
|
}
|
|
|
|
/** On Sandstorm the platform has already authorized the request and injects the
|
|
* user id as a header; there is no Meteor login token in that model at all. */
|
|
export function isSandstormRequest(req) {
|
|
return !!(
|
|
Meteor.settings &&
|
|
Meteor.settings.public &&
|
|
Meteor.settings.public.sandstorm &&
|
|
req && req.headers && req.headers['x-sandstorm-user-id']
|
|
);
|
|
}
|
|
|
|
/** The user id behind this request, or null. Never throws: a caller deciding
|
|
* whether to serve a file wants an answer, not an exception that its own
|
|
* try/catch will turn into a 500. */
|
|
export async function getUserIdFromRequest(req) {
|
|
try {
|
|
if (process.env.NEXTCLOUD_EXAPP === 'true') {
|
|
const { nextcloudIdentity, nextcloudUserId, matchesNextcloudUser } = require('/server/lib/nextcloudIdentity');
|
|
const identity = nextcloudIdentity(req?.headers);
|
|
if (!identity) return null;
|
|
const user = await Meteor.users.findOneAsync(nextcloudUserId(identity));
|
|
return matchesNextcloudUser(user, identity) && user.loginDisabled !== true ? user._id : null;
|
|
}
|
|
if (isSandstormRequest(req)) {
|
|
return req.headers['x-sandstorm-user-id'];
|
|
}
|
|
const raw = extractLoginToken(req);
|
|
if (!raw || typeof raw !== 'string' || raw.length < 10) return null;
|
|
const user = await require('/server/lib/activeUser').activeUserByToken(raw, 'requestUser');
|
|
return user ? user._id : null;
|
|
} catch (error) {
|
|
if (process.env.DEBUG === 'true') {
|
|
console.warn('requestUser: could not resolve the request user:', error);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export default {
|
|
parseCookies,
|
|
parseQuery,
|
|
extractLoginToken,
|
|
isSandstormRequest,
|
|
getUserIdFromRequest,
|
|
};
|