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)) })