const GDPROXY_NAME = "a gdproxy instance"; const ACCENT_COLOR = "#DD65E1"; const SERVICE_ACCOUNTS = [ // Enter service accounts' key herd { "type": "service_account", "project_id": "no", "private_key_id": "noo", "private_key": "hell no", "client_email": "you get the point now i hope", "client_id": "107802968794798221608", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/no" } ]; const DRIVES = [ { // Enter folder id id you want to narrow access to a folder or null "folderId": "1QIrFMjC1ITraZ2xyHjocY9c5LUlFZ-I0", // If null, folderId must not be null "teamDriveId": null, "name": GDPROXY_NAME, "useCache": true, // username:password pairs, empty to disable authentication "auth": [] }, { // Enter folder id if you want to narrow access to a folder or null "folderId": null, // If null, folderId must not be null "teamDriveId": "0AOmq9TyURQtXUk9PVA", "name": "tdroot", "useCache": true, // username:password pairs, empty to disable authentication "auth": [ "username:password", "horny:yes" ] }, { // Enter folder id if you want to narrow access to a folder or null "folderId": "1GrPj3xvy503vAEsuhs5NgogmeKLLMGoU", // If null, folderId must not be null "teamDriveId": "0AOmq9TyURQtXUk9PVA", "name": "tdnotroot", "useCache": false, // username:password pairs, empty to disable authentication "auth": [] } ]; // This is past the config part const CSS = `body { background-color: black; color: white; font-family: sans-serif; padding: 1em; } a { color: ${ACCENT_COLOR}; } div.error { border: 1px solid #fcc; background: #fee; padding: 0.5em 1em 0.5em 1em; color: black; text-align: center; } .text-right { text-align: right; } th { text-align: left; } th:nth-child(1) { width: 100%; }`; const PATH_RE = /^\/(\d+):\/(?:(.+)\/?)?$/; for (let i=0; i < DRIVES.length; i++) { if (DRIVES[i].useCache) { DRIVES[i].pathCache = {}; DRIVES[i].listCache = {}; } } let pageTokenCache = {}; let sa = SERVICE_ACCOUNTS[Math.floor(Math.random() * SERVICE_ACCOUNTS.length)]; let saBearer = null; let saBearerExpiry = -1; let saKey = sa.private_key.substring("-----BEGIN PRIVATE KEY-----".length, sa.private_key.length - "-----END PRIVATE KEY-----".length); // mmm yes no document.createElement function escapeHTML(text) { return text.replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll("\"", """) .replaceAll("'", "'"); } let escapeURL = encodeURIComponent; let unescapeURL = decodeURIComponent; Object.prototype.exists = Object.prototype.hasOwnProperty; function btoaurl(i) { return btoa(i).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", ""); } // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#pkcs_8_import function str2ab(str) { const buf = new ArrayBuffer(str.length); const bufView = new Uint8Array(buf); for (let i = 0, strLen = str.length; i < strLen; i++) { bufView[i] = str.charCodeAt(i); } return buf; } function ab2str(ab) { let str = ""; ab = new Uint8Array(ab); for (let i=0; i < ab.byteLength; i++) { str += String.fromCharCode(ab[i]); } return str; } function buildErrorPage(nonce, title, message) { return ` ${escapeHTML(title)}
${escapeHTML(message)}
`; } // https://stackoverflow.com/a/49361727 // https://stackoverflow.com/a/11832950 function format_bytes(size) { const power = 1000n; let n = 0; const power_labels = ["", "K", "M", "G", "T", "P"]; while (size >= power && power_labels.length - 1 > n) { size /= power; n += 1; } return `${Math.round(Number(size) * 100) / 100} ${power_labels[n]}B`; } async function pathToId(path, drive) { let paths = path.split("/"); let id = drive.folderId; let mimeType = null; for (let i=0; i < paths.length; i++) { path = paths[i]; if (path === "") { continue; } path = unescapeURL(path).replaceAll("\\", "\\\\").replaceAll("'", "\\'"); if (drive.useCache && drive.pathCache.exists(path)) { id = drive.pathCache[path].id; mimeType = drive.pathCache[path].mimeType; continue; } let query = `driveId=${drive.teamDriveId}&q=${escapeURL("name = '" + path + "' and '" + id + "' in parents")}&corpora=drive`; if (drive.teamDriveId !== null) { if (id === null) { query = `driveId=${drive.teamDriveId}&q=${escapeURL("name = '" + path + "' and '" + drive.teamDriveId + "' in parents")}&corpora=drive`; } } else { query = `q=${escapeURL("name = '" + path + "' and '" + id + "' in parents")}&corpora=user`; } let url = `https://www.googleapis.com/drive/v3/files?${query}&includeItemsFromAllDrives=true&pageSize=1&supportsAllDrives=true&fields=files(id,mimeType)`; let resp = await (await fetch(url, {"headers": {"Authorization": "Bearer " + saBearer}})).json(); if (resp.files.length < 1) { return null, null; } id = resp.files[0].id; mimeType = resp.files[0].mimeType; if (drive.useCache) { drive.pathCache[path] = {"id": id, "mimeType": mimeType}; } } return {"id": id, "mimeType": mimeType}; } async function listFiles(id, drive, pageToken) { if (drive.useCache && drive.listCache.exists(`${id},${pageToken || ""}`)) { return drive.listCache[`${id},${pageToken || ""}`]; } let url = `https://www.googleapis.com/drive/v3/files?q=${escapeURL("'" + id + "' in parents")}&includeItemsFromAllDrives=true&supportsAllDrives=true&fields=nextPageToken,files(name,size)&orderBy=folder,name_natural`; if (typeof pageToken !== "undefined") { url += `&pageToken=${escapeURL(pageToken)}`; } url += "&corpora="; if (drive.teamDriveId !== null) { url += `drive&driveId=${drive.teamDriveId}`; } else { url += "user"; } let resp = await (await fetch(url, {"headers": {"Authorization": "Bearer " + saBearer}})).json(); if (drive.useCache) { drive.listCache[`${id},${pageToken || ""}`] = resp; } return resp; } addEventListener("fetch", event => { event.respondWith(handleRequest(event.request)); }); async function handleRequest(request) { let nonceArray = crypto.getRandomValues(new Uint8Array(16)); let nonce = ""; for (let i=0; i < nonceArray.length; i++) { nonce += nonceArray[i].toString(16).padStart(2, "0"); } if (request.method !== "GET") { return new Response(buildErrorPage(nonce, GDPROXY_NAME, "501 Not Implemented"), { status: 501, statusText: "Not Implemented", headers: { "Content-Type": "text/html; charset=utf-8", "Content-Security-Policy": `default-src 'none'; style-src 'nonce-${nonce}'`, "Referrer-Policy": "no-referrer", "Strict-Transport-Security": "max-age=31536000" } }); } let url = new URL(request.url); if (url.protocol !== "https:" || url.pathname === "/") { url.protocol = "https:"; if (url.pathname === "/") { url.pathname = "/0:/"; } let content = ` ${escapeHTML(GDPROXY_NAME)}

Redirecting to ${escapeHTML(url.href)}

`; return new Response(content, { status: 301, statusText: "Moved Permanently", headers: { "Location": url.href, "Content-Type": "text/html; charset=utf-8", "Content-Security-Policy": `default-src 'none'; style-src 'nonce-${nonce}'`, "Referrer-Policy": "no-referrer", "Strict-Transport-Security": "max-age=31536000" } }); } let parsed = url.pathname.match(PATH_RE); if (parsed === null || Number(parsed[1]) >= DRIVES.length) { return new Response(buildErrorPage(nonce, GDPROXY_NAME + " - Not Found", "404 Not Found"), { status: 404, statusText: "Not Found", headers: { "Content-Type": "text/html; charset=utf-8", "Content-Security-Policy": `default-src 'none'; style-src 'nonce-${nonce}'`, "Referrer-Policy": "no-referrer", "Strict-Transport-Security": "max-age=31536000" } }); } let drive = DRIVES[Number(parsed[1])]; if (drive.auth.length !== 0) { let authHeader = request.headers.get("Authorization"); if (!request.headers.has("Authorization") || !authHeader.startsWith("Basic ")) { return new Response(buildErrorPage(nonce, drive.name + " - Unauthorised", "Unauthorised"), { status: 401, statusText: "Unauthorised", headers: { "Content-Type": "text/html; charset=utf-8", "Content-Security-Policy": `default-src 'none'; style-src 'nonce-${nonce}'`, "Referrer-Policy": "no-referrer", "Strict-Transport-Security": "max-age=31536000", "WWW-Authenticate": `Basic realm="${drive.name}"` } }); } authHeader = atob(authHeader.replace("Basic ", "")); let allowed = false; for (let i=0; i < drive.auth.length; i++) { if (authHeader === drive.auth[i]) { allowed = true; break; } } if (!allowed) { return new Response(buildErrorPage(nonce, drive.name + " - Unauthorised", "Unauthorised"), { status: 401, statusText: "Unauthorised", headers: { "Content-Type": "text/html; charset=utf-8", "Content-Security-Policy": `default-src 'none'; style-src 'nonce-${nonce}'`, "Referrer-Policy": "no-referrer", "Strict-Transport-Security": "max-age=31536000", "WWW-Authenticate": `Basic realm="${drive.name}"` } }); } } if (Date.now() >= saBearerExpiry) { if (typeof saKey === "string") { saKey = await crypto.subtle.importKey( "pkcs8", str2ab(atob(saKey.replace("-", ""))), { "name": "RSASSA-PKCS1-v1_5", "hash": "SHA-256" }, false, ["sign"] ); } let iat = Math.floor(Date.now() / 1000); let exp = iat + 60 * 60; let token = btoaurl(JSON.stringify({"alg": "RS256", "typ": "JWT"})); token += "." + btoaurl(JSON.stringify({ "iss": sa.client_email, "aud": sa.token_uri, "scope": "https://www.googleapis.com/auth/drive.readonly", "exp": exp, "iat": iat })); let sig = btoaurl(ab2str(await crypto.subtle.sign("RSASSA-PKCS1-v1_5", saKey, str2ab(token)))); token += "." + sig; let params = new URLSearchParams(); params.append("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"); params.append("assertion", token); let resp = await (await fetch(sa.token_uri, {"method": "POST", "body": params})).json(); saBearer = resp.access_token; saBearerExpiry = (exp - 60) * 1000; } let path = parsed[2]; if (typeof path === "undefined") { path = ""; } else { path = path.replaceAll(/^\/+|\/+$/g, ""); } let baseId = drive.folderId || drive.teamDriveId; let id = baseId; let mimeType = "application/vnd.google-apps.folder"; if (path !== "") { let resp = await pathToId(path, drive); if (resp === null) { return new Response(buildErrorPage(nonce, drive.name + " - Not Found", "404 Not Found"), { status: 404, statusText: "Not Found", headers: { "Content-Type": "text/html; charset=utf-8", "Content-Security-Policy": `default-src 'none'; style-src 'nonce-${nonce}'`, "Referrer-Policy": "no-referrer", "Strict-Transport-Security": "max-age=31536000" } }); } id = resp.id; mimeType = resp.mimeType; } if (mimeType === "application/vnd.google-apps.folder") { let query = new URLSearchParams(url.search); let pagenum = Number(query.get("page") || "1"); let files = await listFiles(id, drive, pageTokenCache[`${id},${pagenum - 1}`]); if (typeof pageTokenCache[`${id},${pagenum - 1}`] === "undefined") { pagenum = 1; } if (typeof files.nextPageToken !== "undefined") { pageTokenCache[`${id},${pagenum}`] = files.nextPageToken; } let page = ` ${escapeHTML(drive.name)} - ${escapeHTML(unescapeURL(path.replace(/.+\//, "")))}

/${parsed[1]}:/${escapeHTML(path)}

`; if (id !== baseId) { let bpath = path.split("/"); bpath.pop(); page += ` `; } for (let i=0; i < files.files.length; i++) { let file = files.files[i]; let name = file.name; let size = file.size; if (typeof size === "undefined") { size = "-"; } else { size = format_bytes(BigInt(size)); } page += ` `; } page += "\n
Name Size
.. -
${escapeHTML(name)} ${size}
"; if (pagenum !== 1 || typeof files.nextPageToken !== "undefined") { page += "\n
"; if (pagenum !== 1) { let prevURL = `/${parsed[1]}:/${path}?page=${pagenum - 1}`; page += `\n Back`; } page += `\n
Page ${pagenum}
`; if (typeof files.nextPageToken !== "undefined") { let nextURL = `/${parsed[1]}:/${path}?page=${pagenum + 1}`; page += `\n
Next
`; } } page += ` `; return new Response(page, { headers: { "Content-Type": "text/html; charset=utf-8", "Content-Security-Policy": `default-src 'none'; style-src 'nonce-${nonce}'`, "Referrer-Policy": "no-referrer", "Strict-Transport-Security": "max-age=31536000" } }); } let reqHeaders = {"Authorization": "Bearer " + saBearer}; if (request.headers.has("Range")) { reqHeaders.Range = request.headers.get("Range"); } let resp = await fetch(`https://www.googleapis.com/drive/v3/files/${id}?alt=media&supportsAllDrives=true`, {"headers": reqHeaders}); let headers = new Headers({"Content-Type": resp.headers.get("Content-Type")}); if (resp.headers.has("Content-Range")) { headers.append("Content-Range", resp.headers.get("Content-Range")); } if (resp.headers.has("Content-Length")) { headers.append("Content-Length", resp.headers.get("Content-Length")); } if (resp.headers.has("Accept-Ranges")) { headers.append("Accept-Ranges", resp.headers.get("Accept-Ranges")); } return new Response(resp.body, { status: resp.status, statusText: resp.statusText, headers: headers }); }