Initial commit

This commit is contained in:
blank X 2021-08-17 17:56:54 +07:00
commit d10cec7aba
Signed by: blankie
GPG Key ID: CC15FC822C7F61F5
5 changed files with 508 additions and 0 deletions

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 blank X
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# gdproxy
A google drive index but no client-side javascript because I hate it

461
index.js Normal file
View File

@ -0,0 +1,461 @@
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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll("\"", "&quot;")
.replaceAll("'", "&#x27;");
}
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 `<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>${escapeHTML(title)}</title>
<style nonce="${nonce}">${CSS}</style>
</head>
<body>
<div class="error">${escapeHTML(message)}</div>
</body>
</html>`;
}
// 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}&nbsp;${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 = `<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>${escapeHTML(GDPROXY_NAME)}</title>
<style nonce="${nonce}">${CSS}</style>
</head>
<body>
<p>Redirecting to <a href="${escapeHTML(url.href)}">${escapeHTML(url.href)}</a></p>
</body>
</html>`;
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 = `<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>${escapeHTML(drive.name)} - ${escapeHTML(unescapeURL(path.replace(/.+\//, "")))}</title>
<style nonce="${nonce}">${CSS}</style>
</head>
<body>
<h3>/${parsed[1]}:/${escapeHTML(path)}</h3>
<table>
<tr>
<th>Name</th>
<th>Size</th>
</tr>`;
if (id !== baseId) {
let bpath = path.split("/");
bpath.pop();
page += `
<tr>
<td><a href="/${parsed[1]}:/${bpath.join("/")}">..</a></td>
<td>-</td>
</tr>`;
}
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 += `
<tr>
<td><a href="/${parsed[1]}:/${path}/${escapeURL(name)}">${escapeHTML(name)}</a></td>
<td>${size}</td>
</tr>`;
}
page += "\n </table>";
if (pagenum !== 1 || typeof files.nextPageToken !== "undefined") {
page += "\n <hr />";
if (pagenum !== 1) {
let prevURL = `/${parsed[1]}:/${path}?page=${pagenum - 1}`;
page += `\n <a href="${prevURL}">Back</a>`;
}
page += `\n <center>Page ${pagenum}</center>`;
if (typeof files.nextPageToken !== "undefined") {
let nextURL = `/${parsed[1]}:/${path}?page=${pagenum + 1}`;
page += `\n <div class="text-right"><a href="${nextURL}">Next</a></div>`;
}
}
page += `
</body>
</html>`;
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
});
}

16
package.json Normal file
View File

@ -0,0 +1,16 @@
{
"private": false,
"name": "gdproxy",
"version": "1.0.0",
"description": "Exposes Google Drive folders and files",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"format": "prettier --write '**/*.{js,css,json,md}'"
},
"author": "blank X <theblankx@protonmail.com>",
"license": "MIT",
"devDependencies": {
"prettier": "^1.18.2"
}
}

7
wrangler.toml Normal file
View File

@ -0,0 +1,7 @@
name = "gdproxy"
type = "javascript"
account_id = ""
workers_dev = true
route = ""
zone_id = ""