bcm-auth/index.js

134 lines
3.7 KiB
JavaScript

const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
const KV = {
'skydusky': SKYDUSKY
};
const noauth = ['lidar'];
/************************************************************/
/*
* Most of this code is based on
* https://developers.cloudflare.com/workers/examples/basic-auth
*/
/**
* Parse HTTP Basic Authorization value.
* @param {Request} request
* @throws {BadRequestException}
* @returns {{ user: string, pass: string }}
*/
function basicAuthentication(request) {
const Authorization = request.headers.get('Authorization')
const [scheme, encoded] = Authorization.split(' ')
// The Authorization header must start with Basic, followed by a space.
if (!encoded || scheme !== 'Basic') {
throw new BadRequestException('Malformed authorization header.')
}
// Decodes the base64 value and performs unicode normalization.
// @see https://datatracker.ietf.org/doc/html/rfc7613#section-3.3.2 (and #section-4.2.2)
// @see https://dev.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/normalize
const buffer = Uint8Array.from(atob(encoded), character => character.charCodeAt(0))
const decoded = new TextDecoder().decode(buffer).normalize()
// The username & password are split by the first colon.
//=> example: "username:password"
const index = decoded.indexOf(':')
// The user & password are split by the first colon and MUST NOT contain control characters.
// @see https://tools.ietf.org/html/rfc5234#appendix-B.1 (=> "CTL = %x00-1F / %x7F")
if (index === -1 || /[\0-\x1F\x7F]/.test(decoded)) {
throw new BadRequestException('Invalid authorization value.')
}
return {
user: decoded.substring(0, index),
pass: decoded.substring(index + 1),
}
}
function UnauthorizedException(reason) {
return new Response(reason, {
status: 401,
statusTest: 'Unauthorized'
});
}
function BadRequestException(reason) {
this.status = 400
this.statusText = 'Bad Request'
this.reason = reason
}
/**
* Throws exception on verification failure.
* @param {string} user
* @param {string} pass
* @throws {UnauthorizedException}
*/
async function verifyCredentials(store, user, pass) {
if (!KV.hasOwnProperty(store)) {
throw new UnauthorizedException('Invalid password.');
}
const KVpass = await KV[store].get(user);
if (KVpass === 'null' || KVpass !== pass) {
throw new UnauthorizedException('Invalid password.');
}
}
/************************************************************/
/**
* Main functions to handle request
*/
async function handleRequest(request) {
const requestURL = new URL(request.url);
const path = requestURL.pathname.substring(1);
const area = path.split('/')[0];
if (!noauth.includes(area)) {
// Prompt login
if (!request.headers.has('Authorization')) {
return new Response('Please login.', {
status: 401,
headers: { 'WWW-Authenticate': `Basic realm="${area.toUpperCase()}", charset="UTF-8"` }
});
}
// Verify login
const { user, pass } = basicAuthentication(request);
try {
await verifyCredentials(area, user, pass);
} catch (e) {
return e;
}
}
// S3 Client
const client = new S3Client({
credentials: {
accessKeyId: WASABI_ACCESS_KEY,
secretAccessKey: WASABI_SECRET_KEY,
},
endpoint: 'https://s3.us-east-2.wasabisys.com',
region: 'us-east-2',
});
// S3 Request
const command = new GetObjectCommand({
Bucket: 'bigcavemaps.com',
Key: path,
});
// S3 Url
const url = await getSignedUrl(client, command, { expiresIn: 3600 });
const r = await fetch(url);
return r;
}
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})