Skip to content

Instantly share code, notes, and snippets.

@Erisa
Last active April 22, 2024 02:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Erisa/a592ce4d81934d65e7f1ac46dcfcc2fd to your computer and use it in GitHub Desktop.
Save Erisa/a592ce4d81934d65e7f1ac46dcfcc2fd to your computer and use it in GitHub Desktop.
Dynamically generate OTA manifests for iOS apps stored in Cloudflare R2
export default {
async fetch(request, env, ctx) {
let url = new URL(request.url)
let fields = url.pathname.split('/')
const task = fields[1]
const bundleid = fields[2]
if (task === "install") {
return Response.redirect(`itms-services://?action=download-manifest&url=https://${url.hostname}/manifest/${bundleid}`)
} else if (task === "manifest") {
const plist = `
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>items</key>
<array>
<dict>
<key>assets</key>
<array>
<dict>
<key>kind</key>
<string>software-package</string>
<key>url</key>
<string>https://cdn.erisa.cloud/ios-library/${bundleid}.ipa</string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>bundle-identifier</key>
<string>${bundleid}</string>
<key>kind</key>
<string>software</string>
<key>title</key>
<string>${bundleid}</string>
</dict>
</dict>
</array>
</dict>
</plist>
`
return new Response(plist, {headers: {"content-type": "application/xml"}})
}
if (url.pathname === '/') {
const path = "ios-library/"
let listing = await env.R2_BUCKET.list({ prefix: path })
console.log(JSON.stringify(listing.objects))
let html = "";
let lastModified = null;
if (request.method === "GET") {
let htmlList = [];
for (let file of listing.objects) {
let name = file.key.substring(path.length, file.key.length)
if (name.startsWith(".") && env.HIDE_HIDDEN_FILES) continue;
let dateStr = file.uploaded.toISOString()
dateStr = dateStr.split('.')[0].replace('T', ' ')
dateStr = dateStr.slice(0, dateStr.lastIndexOf(':')) + 'Z'
htmlList.push(
` <tr>` +
`<td><a href="https://${url.hostname}/install/${encodeURIComponent(name.substring(0, name.lastIndexOf(".")))}">${name}</a></td>` +
`<td>${dateStr}</td><td>${niceBytes(file.size)}</td></tr>`);
if (lastModified == null || file.uploaded > lastModified) {
lastModified = file.uploaded;
}
}
html = `<!DOCTYPE html>
<html>
<head>
<title>Index of ${path}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="utf-8">
<style type="text/css">
td { padding-right: 16px; text-align: right; font-family: monospace }
td:nth-of-type(1) { text-align: left; overflow-wrap: anywhere }
td:nth-of-type(3) { white-space: nowrap }
th { text-align: left; }
@media (prefers-color-scheme: dark) {
body {
color: white;
background-color: #1c1b22;
}
a {
color: #3391ff;
}
a:visited {
color: #3391ff;
}
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 200;
unicode-range: U+0-7C,U+7D-FFFF;
src: local(''),
url('https://lewd.tech/fonts/inter-v8-latin-regular.woff2') format('woff2') /* Chrome 26+, Opera 23+, Firefox 39+ */
}
body {
margin: 40px auto;
max-width: 650px;
line-height: 1.6;
font-size: 16px;
padding: 0 10px;
font-family: "Inter",
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Helvetica,
Arial,
sans-serif,
"Apple Color Emoji",
"Segoe UI Emoji";
}
a {
color: #3391ff;
text-decoration: none;
}
a:hover {
color:#4882e0;
}
h1, h2, h3 {
line-height: 1.2;
}
.erisa {
color: #C63B65;
}
</style>
</head>
<body>
<h1 class="erisa">Index of ${path}</h1>
<table>
<tr><th>Filename</th><th>Modified</th><th>Size</th></tr>
${htmlList.join("\n")}
</table>
</body>
</html>
`
};
return new Response(html === "" ? null : html, {
status: 200,
headers: {
"access-control-allow-origin": "",
"last-modified": lastModified === null ? "" : lastModified.toUTCString(),
"content-type": "text/html",
}
});
} else {
return new Response("invalid path", {status: 404})
}
},
};
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
function niceBytes(x) {
let l = 0, n = parseInt(x.toString(), 10) || 0;
while (n >= 1000 && ++l) {
n = n / 1000;
}
return (n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l]);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment