-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 4dc729e
Showing
7 changed files
with
210 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
{ | ||
"$schema": "https://raw.githubusercontent.com/skymethod/denoflare/v0.5.11/common/config.schema.json", | ||
"scripts": { | ||
"noql-apt": { | ||
"path": "worker.ts", | ||
"localPort": 3030, | ||
"bindings": { | ||
"allowCorsOrigins": { "value": "apt.noql.net" } | ||
}, | ||
"customDomains": [ "apt.noql.net" ] | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"deno.enable": true, | ||
"deno.unstable": true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# apt-cf-worker |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export type { IncomingRequestCf, R2Bucket, R2Object, R2Objects, R2ObjectBody, R2ListOptions } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.5.11/common/cloudflare_workers_types.d.ts'; | ||
export { encodeXml } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.5.11/common/xml_util.ts'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { encodeXml, R2Objects } from './deps.ts'; | ||
|
||
export function computeDirectoryListingHtml(objects: R2Objects, opts: { prefix: string, cursor?: string }): string { | ||
const { prefix, cursor } = opts; | ||
const lines = ['<!DOCTYPE html>', '<html>', '<head>', '<style>', STYLE, '</style>', '</head>', '<body>']; | ||
|
||
lines.push('<div id="contents">'); | ||
lines.push(`<div class="full">${computeBreadcrumbs(prefix)}</div>`); | ||
lines.push('<div class="full"> </div>'); | ||
lines.push(`<div>Name</div><div class="ralign">Size (bytes)</div>`); | ||
if (objects.delimitedPrefixes.length > 0) { | ||
for (const delimitedPrefix of objects.delimitedPrefixes) { | ||
lines.push(`<a class="full" href="${encodeXml('/' + delimitedPrefix)}">${encodeXml(delimitedPrefix.substring(prefix.length))}</a>`); | ||
} | ||
lines.push('<div class="full"> </div>'); | ||
} | ||
for (const obj of objects.objects) { | ||
lines.push(`<a href="${encodeXml('/' + obj.key)}">${encodeXml(obj.key.substring(prefix.length))}</a><div class="ralign">${obj.size.toLocaleString()}</div>`); | ||
} | ||
if (cursor) { | ||
lines.push('<div class="full"> </div>'); | ||
lines.push(`<div class="full"><a href="?cursor=${encodeXml(cursor)}">next ➜</a></div>`); | ||
} | ||
lines.push('</div>'); | ||
|
||
lines.push('</body>', '</html>'); | ||
return lines.join('\n'); | ||
} | ||
|
||
const STYLE = ` | ||
body { margin: 3rem; font-family: sans-serif; } | ||
a { text-decoration: none; text-underline-offset: 0.2rem; } | ||
a:hover { text-decoration: underline; } | ||
.ralign { text-align: right; } | ||
#contents { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem 1.5rem; white-space: nowrap; } | ||
#contents .full { grid-column: 1 / span 2; } | ||
@media (prefers-color-scheme: dark) { | ||
body {background: #151920; color: #f5f5f5; } | ||
a { color: #3bb13b; } | ||
} | ||
`; | ||
|
||
function computeBreadcrumbs(prefix: string): string { | ||
const tokens = ('/' + prefix).split('/').filter((v, i) => i === 0 || v !== ''); | ||
return tokens.map((v, i) => `${i === 0 ? '' : ` ⟩ `}${i === tokens.length - 1 ? (i === 0 ? 'root' : encodeXml(v)) : `<a href="${tokens.slice(0, i + 1).join('/') + '/'}">${i === 0 ? 'root' : encodeXml(v)}</a>`}`).join(''); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
import { IncomingRequestCf, R2Object, R2ObjectBody, R2ListOptions } from './deps.ts'; | ||
import { computeDirectoryListingHtml } from './listing.ts'; | ||
import { WorkerEnv } from './worker_env.d.ts'; | ||
|
||
export default { | ||
async fetch(request: IncomingRequestCf, env: WorkerEnv): Promise<Response> { | ||
try { | ||
return await computeResponse(request, env); | ||
} catch (e) { | ||
if (typeof e === 'object' && tryParseMessageCode(e.message) === 10039) { // The requested range is not satisfiable (10039) | ||
return new Response(e.message, { status: 416 }); | ||
} | ||
return new Response(`${e.stack || e}`, { status: 500 }); | ||
} | ||
} | ||
}; | ||
|
||
declare global { | ||
interface ResponseInit { | ||
// non-standard cloudflare property, defaults to 'auto' | ||
encodeBody?: 'auto' | 'manual'; | ||
} | ||
} | ||
|
||
const DIR_LIST_LIMIT = 1000; | ||
|
||
function tryParseMessageCode(message: unknown): number | undefined { | ||
// The requested range is not satisfiable (10039) | ||
const m = /^.*?\((\d+)\)$/.exec(typeof message === 'string' ? message : ''); | ||
return m ? parseInt(m[1]) : undefined; | ||
} | ||
|
||
async function computeResponse(request: IncomingRequestCf, env: WorkerEnv): Promise<Response> { | ||
const bucket = env.bucket; | ||
const allowCorsOrigins = stringSetFromCsv(env.allowCorsOrigins); | ||
|
||
const { method, url, headers } = request; | ||
|
||
if (method !== 'GET' && method !== 'HEAD') { | ||
return new Response(`Method '${method}' not allowed`, { status: 405 }); | ||
} | ||
|
||
const { pathname, searchParams } = new URL(url); | ||
let key = pathname.substring(1); // strip leading slash | ||
key = decodeURIComponent(key); | ||
|
||
// special handling for robots.txt | ||
if (key === 'robots.txt') { | ||
return new Response(method === 'GET' ? 'User-agent: *\nDisallow: /' : undefined, { headers: { 'content-type': 'text/plain; charset=utf-8' } }); | ||
} | ||
|
||
let obj: R2Object | null = null; | ||
const getOrHead: (key: string) => Promise<R2Object | null> = (key) => { | ||
return method === 'GET' ? bucket.get(key) : bucket.head(key); | ||
}; | ||
|
||
|
||
// first, try to request the object at the given key | ||
obj = key === '' ? null : await getOrHead(key); | ||
if (obj) { | ||
const accessControlAllowOrigin = computeAccessControlAllowOrigin(obj, headers.get('origin') ?? undefined, allowCorsOrigins); | ||
return computeObjResponse(obj, 200, accessControlAllowOrigin); | ||
} | ||
|
||
// R2 object not found, try listing a directory | ||
let prefix = pathname.substring(1); | ||
let redirect = false; | ||
if (prefix !== '' && !prefix.endsWith('/')) { | ||
prefix += '/'; | ||
redirect = true; | ||
} | ||
|
||
const options: R2ListOptions = { delimiter: '/', limit: DIR_LIST_LIMIT, prefix: prefix === '' ? undefined : prefix, cursor: searchParams.get('cursor') || undefined }; | ||
const objects = await bucket.list(options); | ||
if (objects.delimitedPrefixes.length > 0 || objects.objects.length > 0) { | ||
const { cursor } = objects; | ||
return redirect ? temporaryRedirect({ location: '/' + prefix }) : new Response(computeDirectoryListingHtml(objects, { prefix, cursor }), { headers: { 'content-type': 'text/html; charset=utf-8' } }); | ||
} | ||
|
||
return notFound(method); | ||
} | ||
|
||
function stringSetFromCsv(value: string | undefined) { | ||
return new Set((value ?? '').split(',').map(v => v.trim()).filter(v => v !== '')); | ||
} | ||
|
||
function notFound(method: string): Response { | ||
return new Response(method === 'HEAD' ? undefined : 'not found', { status: 404, headers: { 'content-type': 'text/plain; charset=utf-8' } }); | ||
} | ||
|
||
function temporaryRedirect(opts: { location: string }): Response { | ||
const { location } = opts; | ||
return new Response(undefined, { status: 307, headers: { 'location': location } }); | ||
} | ||
|
||
function isR2ObjectBody(obj: R2Object): obj is R2ObjectBody { | ||
return 'body' in obj; | ||
} | ||
|
||
function computeObjResponse(obj: R2Object, status: number, accessControlAllowOrigin?: string): Response { | ||
let body: ReadableStream | undefined; | ||
if (isR2ObjectBody(obj)) { | ||
body = obj.body; | ||
} | ||
|
||
const headers = new Headers(); | ||
// writes content-type, content-encoding, content-disposition, i.e. the values from obj.httpMetadata | ||
obj.writeHttpMetadata(headers); | ||
|
||
// obj.size represents the full size, but seems to be clamped by the cf frontend down to the actual number of bytes in the partial response | ||
// exactly what we want in a content-length header | ||
headers.set('content-length', String(obj.size)); | ||
|
||
if (accessControlAllowOrigin) headers.set('access-control-allow-origin', accessControlAllowOrigin); | ||
|
||
// non-standard cloudflare ResponseInit property indicating the response is already encoded | ||
// required to prevent the cf frontend from double-encoding it, or serving it encoded without a content-encoding header | ||
const encodeBody = headers.has('content-encoding') ? 'manual' : undefined; | ||
return new Response(body, { status, headers, encodeBody }); | ||
} | ||
|
||
function computeAccessControlAllowOrigin(obj: R2Object, requestOrigin: string | undefined, allowCorsOrigins: Set<string>): string | undefined { | ||
// is request origin allowed? | ||
if (allowCorsOrigins.size === 0) { | ||
return undefined; | ||
} | ||
|
||
if (allowCorsOrigins.has('*')) { | ||
return '*'; | ||
} | ||
|
||
if (requestOrigin && allowCorsOrigins.has(requestOrigin)) { | ||
return requestOrigin; | ||
} | ||
|
||
return undefined; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { R2Bucket } from './deps.ts'; | ||
|
||
export interface WorkerEnv { | ||
readonly bucket: R2Bucket; | ||
readonly allowCorsOrigins?: string; | ||
} |