|
|
|
@ -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("&", "&")
|
|
|
|
|
.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 `<!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} ${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
|
|
|
|
|
});
|
|
|
|
|
}
|