2021-08-17 10:56:54 +00:00
const GDPROXY _NAME = "a gdproxy instance" ;
const ACCENT _COLOR = "#DD65E1" ;
const SERVICE _ACCOUNTS = [
2021-08-17 11:02:54 +00:00
// Enter service accounts' key here
2021-08-17 10:56:54 +00:00
{
"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 : 1 em ;
}
a {
color : $ { ACCENT _COLOR } ;
}
div . error {
border : 1 px solid # fcc ;
background : # fee ;
padding : 0.5 em 1 em 0.5 em 1 em ;
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 ) } < / t i t l e >
< style nonce = "${nonce}" > $ { CSS } < / s t y l e >
< / h e a d >
< body >
< div class = "error" > $ { escapeHTML ( message ) } < / d i v >
< / b o d y >
< / h t m l > ` ;
}
// https://stackoverflow.com/a/49361727
// https://stackoverflow.com/a/11832950
function format _bytes ( size ) {
const power = 1000 n ;
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 ) } < / t i t l e >
< style nonce = "${nonce}" > $ { CSS } < / s t y l e >
< / h e a d >
< body >
< p > Redirecting to < a href = "${escapeHTML(url.href)}" > $ { escapeHTML ( url . href ) } < / a > < / p >
< / b o d y >
< / h t m l > ` ;
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 ( /.+\// , "" ) ) ) } < / t i t l e >
< style nonce = "${nonce}" > $ { CSS } < / s t y l e >
< / h e a d >
< body >
< h3 > /${parsed[1]}:/ $ { escapeHTML ( path ) } < / h 3 >
< table >
< tr >
< th > Name < / t h >
< th > Size < / t h >
< / t r > ` ;
if ( id !== baseId ) {
let bpath = path . split ( "/" ) ;
bpath . pop ( ) ;
page += `
< tr >
< td > < a href = "/${parsed[1]}:/${bpath.join(" / ")}" > . . < / a > < / t d >
< td > - < / t d >
< / t r > ` ;
}
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 > < / t d >
< td > $ { size } < / t d >
< / t r > ` ;
}
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 += `
< / b o d y >
< / h t m l > ` ;
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
} ) ;
}