UNPKG

@nephele/authenticator-htpasswd

Version:

Apache htpasswd based authenticator for the Nephele WebDAV server.

134 lines 5.39 kB
import crypto from 'node:crypto'; import fsp from 'node:fs/promises'; import { constants } from 'node:fs'; import basicAuth from 'basic-auth'; import apacheMD5 from 'apache-md5'; import crypt from 'apache-crypt'; import bcrypt from 'bcrypt'; import { ForbiddenError, InternalServerError, UnauthorizedError, ResourceNotFoundError, } from 'nephele'; import User from './User.js'; export default class Authenticator { constructor({ realm = 'Nephele WebDAV Service', unauthorizedAccess = false, authUserFilename = '.htpasswd', authUserFile = undefined, blockAuthUserFilename = true, } = {}) { this.realm = realm; this.unauthorizedAccess = unauthorizedAccess; this.authUserFilename = authUserFilename; this.authUserFile = authUserFile; this.blockAuthUserFilename = blockAuthUserFilename; } async authenticate(request, response) { const authorization = request.get('Authorization'); let username = ''; let password = ''; if (this.authUserFile == null && this.blockAuthUserFilename && (request.path.endsWith(`/${this.authUserFilename}`) || request.path.endsWith(`/${this.authUserFilename}/`))) { throw new ForbiddenError("You don't have permission to access this resource."); } if (authorization) { const auth = basicAuth.parse(authorization); if (auth) { username = auth.name; password = auth.pass; } } try { if (username.trim() === '') { throw new UnauthorizedError('Authentication is required to use this server.'); } const htpasswd = await this._getHtpasswdFile(request, response); if (!(await this._checkHtpasswd(username, password, htpasswd))) { throw new UnauthorizedError('The provided credentials are not correct.'); } return new User({ username }); } catch (e) { if (e instanceof UnauthorizedError) { response.set('WWW-Authenticate', `Basic realm="${this.realm}", charset="UTF-8"`); } if (this.unauthorizedAccess) { return new User({ username: 'nobody' }); } throw e; } } async cleanAuthentication(_request, _response) { return; } async _checkHtpasswd(username, password, htpasswd) { let lines = htpasswd.split('\n'); for (let line of lines) { const [user, digest, extra] = line.split(':'); if (extra) { continue; } if (user === username) { return await this._checkPassword(digest, password); } } return false; } async _checkPassword(digest, password) { if (digest.startsWith('$apr1$')) { return digest === apacheMD5(password, digest); } else if (digest.startsWith('{SHA}')) { let hash = crypto.createHash('sha1'); hash.update(password); return '{SHA}' + hash.digest('base64') === digest; } else if (digest.startsWith('$2y$')) { return await bcrypt.compare(password, '$2b$' + digest.substring(4)); } return digest === password || crypt(password, digest) === digest; } async _getHtpasswdFile(request, response) { if (this.authUserFile != null) { try { await fsp.access(this.authUserFile, constants.F_OK); return await fsp.readFile(this.authUserFile, { encoding: 'utf-8' }); } catch (e) { throw new InternalServerError("The server's authentication user file is not accessible."); } } const adapter = response.locals.adapter; const baseUrl = response.locals.baseUrl; const urlParts = request.path .substring(baseUrl.pathname.length) .replace(/(?:^\/|\/$)/g, '') .split('/'); for (let i = 0; i < urlParts.length; i++) { const htpasswdUrl = new URL(`${[...urlParts.slice(0, i), ''].join('/')}${this.authUserFilename}`, baseUrl); let htpasswdResource = undefined; try { htpasswdResource = await adapter.getResource(htpasswdUrl, baseUrl); } catch (e) { if (e instanceof ResourceNotFoundError) { continue; } throw e; } if (htpasswdResource != null) { try { const stream = await htpasswdResource.getStream(); return await this._streamToString(stream); } catch (e) { throw new InternalServerError("The server's authentication user file is not accessible."); } } } return ''; } _streamToString(stream) { const chunks = []; return new Promise((resolve, reject) => { stream.on('data', (chunk) => chunks.push(Buffer.from(chunk))); stream.on('error', (err) => reject(err)); stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))); }); } } //# sourceMappingURL=Authenticator.js.map