├── src ├── robots.txt ├── _worker.ts ├── cfw.d.ts ├── filter │ ├── user.ts │ ├── repo.ts │ └── index.ts ├── index.ts ├── routes │ ├── gist.githubusercontent.com.ts │ ├── index.ts │ ├── raw.githubusercontent.com.ts │ ├── codeload.github.com.ts │ └── github.com.ts ├── 404.html ├── index.html ├── favicon.svg └── style.css ├── wrangler.toml ├── bun.lockb ├── .env.example ├── eslint.config.js ├── renovate.json ├── .editorconfig ├── .github └── workflows │ ├── size-limit.yml │ └── lint.yml ├── README.md ├── tsconfig.json ├── LICENSE ├── package.json └── .gitignore /src/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Disallow: / -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "parcel build" 3 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mchaNetwork/github-proxy/HEAD/bun.lockb -------------------------------------------------------------------------------- /src/_worker.ts: -------------------------------------------------------------------------------- 1 | import {start} from 'worktop/cfw'; 2 | import app from '.'; 3 | 4 | export default start(app.run); 5 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | GHP_BLOCKED_USERS=exact-user,/^regex-user$/,/partial-user/ 2 | GHP_BLOCKED_REPOS=exact/repo,/^regex/repo$/,/partial/repo/ 3 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import {configBuilder} from '@mochaa/eslintrc'; 2 | 3 | export default configBuilder({}, { 4 | rules: { 5 | 'node/prefer-global/process': 'off', 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /src/cfw.d.ts: -------------------------------------------------------------------------------- 1 | import type {Context} from 'worktop'; 2 | 3 | declare global { 4 | type Bindings = { 5 | bindings: { 6 | ASSETS: Fetcher; 7 | }; 8 | } & Context; 9 | 10 | const process: { 11 | env: Record; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:js-lib" 4 | ], 5 | "packageRules": [ 6 | { 7 | "matchUpdateTypes": [ 8 | "minor", 9 | "patch", 10 | "pin", 11 | "digest" 12 | ], 13 | "automerge": true 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/filter/user.ts: -------------------------------------------------------------------------------- 1 | import {type Condition, matcher} from '.'; 2 | 3 | const users = (process.env.GHP_BLOCKED_USERS ?? '').split(','); 4 | const rules = users.map(pattern => matcher(pattern)); 5 | 6 | const disallow: Condition = user => rules.some(m => m(user)); 7 | 8 | export default disallow; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = false 9 | insert_final_newline = true 10 | 11 | [package.json] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.{yml,yaml}] 16 | indent_style = space 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.github/workflows/size-limit.yml: -------------------------------------------------------------------------------- 1 | name: size 2 | on: 3 | pull_request: 4 | branches: 5 | - trunk 6 | jobs: 7 | size: 8 | runs-on: ubuntu-latest 9 | env: 10 | CI_JOB_NUMBER: 1 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: andresz1/size-limit-action@v1 14 | with: 15 | github_token: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # github-proxy 2 | 3 | Properly implemented proxy for GitHub running on Cloudflare Workers, 4 | trying to be feature-rich while not bloated. 5 | 6 | ## Acknowledgements 7 | 8 | - [hunshcn/gh-proxy](https://github.com/hunshcn/gh-proxy) 9 | - [FastGit](https://github.com/FastGitORG/cfworker) for stripping down the original worker script. 10 | 11 | ## License 12 | 13 | MIT 14 | -------------------------------------------------------------------------------- /src/filter/repo.ts: -------------------------------------------------------------------------------- 1 | import {type Condition, matcher} from '.'; 2 | 3 | type Repo = { 4 | user: string; 5 | repo: string; 6 | }; 7 | 8 | const repos = (process.env.GHP_BLOCKED_REPOS ?? '').split(','); 9 | const rules = repos.map(pattern => matcher(pattern)); 10 | 11 | const forbid: Condition = ({user, repo}) => rules.some(m => m(`${user}/${repo}`)); 12 | 13 | export default forbid; 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {Router} from 'worktop'; 2 | import routes, {bind} from './routes'; 3 | 4 | const app = new Router(); 5 | 6 | app.mount('/https:/', routes); 7 | app.mount('/http:/', routes); 8 | bind(app); 9 | 10 | // eslint-disable-next-line regexp/no-empty-group 11 | app.add('GET', /(?:)/, async (request, context) => context.bindings.ASSETS.fetch(request)); 12 | 13 | export default app; 14 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: 3 | push: 4 | pull_request: 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: oven-sh/setup-bun@v2 11 | - uses: actions/cache@v4 12 | id: cache-packages 13 | with: 14 | path: | 15 | ~/.bun/install/cache 16 | key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }} 17 | - run: bun i --frozen-lockfile 18 | - run: bun lint 19 | -------------------------------------------------------------------------------- /src/routes/gist.githubusercontent.com.ts: -------------------------------------------------------------------------------- 1 | import {Router} from 'worktop'; 2 | import {reply} from 'worktop/response'; 3 | import forbidUser from '../filter/user'; 4 | 5 | const app = new Router(); 6 | 7 | app.add('GET', '/:user/:gist/raw/*', async (_, context) => { 8 | const {user, gist, '*': path} = context.params; 9 | if (forbidUser(user)) { 10 | return reply(403); 11 | } 12 | 13 | return fetch(`https://gist.githubusercontent.com/${user}/${gist}/raw/${path}`); 14 | }); 15 | 16 | export default app; 17 | -------------------------------------------------------------------------------- /src/filter/index.ts: -------------------------------------------------------------------------------- 1 | export type Condition = (value: T) => boolean; 2 | 3 | /** Convert strings in rule to a case-insensitive Condition. */ 4 | export function matcher(pattern: string): Condition { 5 | if (pattern.length >= 2 6 | && pattern.startsWith('/') 7 | && pattern.endsWith('/') 8 | ) { 9 | const regex = new RegExp(pattern.slice(1, -1), 'i'); 10 | 11 | return (value: string) => regex.test(value); 12 | } 13 | 14 | const _pattern = pattern.toLowerCase(); 15 | return (value: string) => _pattern === value.toLowerCase(); 16 | } 17 | -------------------------------------------------------------------------------- /src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import {Router} from 'worktop'; 2 | import codeload from './codeload.github.com'; 3 | import gist from './gist.githubusercontent.com'; 4 | import github from './github.com'; 5 | import raw from './raw.githubusercontent.com'; 6 | 7 | export const bind = (app: Router) => { 8 | app.mount('/github.com/', github); 9 | app.mount('/codeload.github.com/', codeload); 10 | app.mount('/raw.githubusercontent.com/', raw); 11 | app.mount('/gist.githubusercontent.com/', gist); 12 | }; 13 | 14 | const app = new Router(); 15 | 16 | bind(app); 17 | 18 | export default app; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": [ 5 | "ESNext", 6 | "WebWorker" 7 | ], 8 | "moduleDetection": "force", 9 | "module": "ESNext", 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "types": [ 13 | "@cloudflare/workers-types" 14 | ], 15 | "allowJs": true, 16 | "checkJs": true, 17 | "strict": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "noEmit": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "verbatimModuleSyntax": true, 22 | /* Linting */ 23 | "skipLibCheck": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/routes/raw.githubusercontent.com.ts: -------------------------------------------------------------------------------- 1 | import {Router} from 'worktop'; 2 | import {reply} from 'worktop/response'; 3 | import forbidRepo from '../filter/repo'; 4 | import forbidUser from '../filter/user'; 5 | 6 | export async function raw(user: string, repo: string, path: string) { 7 | return fetch(`https://raw.githubusercontent.com/${user}/${repo}/${path}`); 8 | } 9 | 10 | const app = new Router(); 11 | 12 | app.add('GET', '/:user/:repo/*', async (_, context) => { 13 | const {user, repo, '*': path} = context.params; 14 | if (forbidUser(user) || forbidRepo({user, repo})) { 15 | return reply(403); 16 | } 17 | 18 | return raw(user, repo, path); 19 | }); 20 | 21 | export default app; 22 | -------------------------------------------------------------------------------- /src/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Not found 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

404. That’s an error.

18 |

The requested URL was not found on this server.

19 |

That’s all we know.

20 |
21 |
22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | GitHub Proxy 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

🐙 GitHub Proxy

18 |
19 |
20 | 21 | 24 |
25 |
26 | 30 |
31 |
32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/routes/codeload.github.com.ts: -------------------------------------------------------------------------------- 1 | import {Router} from 'worktop'; 2 | import {reply} from 'worktop/response'; 3 | import forbidRepo from '../filter/repo'; 4 | import forbidUser from '../filter/user'; 5 | 6 | export type ArchiveFormat = 7 | | 'tar.gz' 8 | | 'zip' 9 | | 'legacy.tar.gz' 10 | | 'legacy.zip'; 11 | 12 | export async function codeload(user: string, repo: string, format: ArchiveFormat, reference: string) { 13 | return fetch(`https://codeload.github.com/${user}/${repo}/${format}/${reference}`); 14 | } 15 | 16 | const app = new Router(); 17 | 18 | app.add('GET', '/:user/:repo/:format/*', async (_, context) => { 19 | const {user, repo, format, '*': reference} = context.params; 20 | if (forbidUser(user) || forbidRepo({repo, user})) { 21 | return reply(403); 22 | } 23 | 24 | switch (format) { 25 | case 'tar.gz': 26 | case 'zip': 27 | case 'legacy.tar.gz': 28 | case 'legacy.zip': { 29 | break; 30 | } 31 | 32 | default: { 33 | return reply(400, '400: Invalid request'); 34 | } 35 | } 36 | 37 | return codeload(user, repo, format, reference); 38 | }); 39 | 40 | export default app; 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Zephyr Lykos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | https://github.com/twitter/twemoji 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-proxy", 3 | "type": "module", 4 | "private": true, 5 | "description": "GitHub proxy on Cloudflare Workers", 6 | "author": "mochaaP ", 7 | "license": "MIT", 8 | "repository": "https://github.com/mchaNetwork/github-proxy", 9 | "main": "dist/_worker.js", 10 | "scripts": { 11 | "build": "rimraf dist && parcel build", 12 | "deploy": "bun run test && bun run build && wrangler pages deploy dist", 13 | "lint": "eslint .", 14 | "size": "size-limit", 15 | "start": "wrangler pages dev dist --compatibility-date=2024-12-01", 16 | "test": "bun run lint && bun run size" 17 | }, 18 | "browserslist": "Firefox ESR, iOS >= 12.5, Chrome >= 87", 19 | "dependencies": { 20 | "nord": "nordtheme/nord", 21 | "ress": "^5.0.2", 22 | "worktop": "next" 23 | }, 24 | "devDependencies": { 25 | "@cloudflare/workers-types": "4.20251213.0", 26 | "@mochaa/eslintrc": "0.1.12", 27 | "@size-limit/preset-small-lib": "11.2.0", 28 | "eslint": "9.39.2", 29 | "parcel": "2.16.3", 30 | "rimraf": "6.1.2", 31 | "size-limit": "11.2.0", 32 | "svgo": "3.3.2", 33 | "typescript": "5.9.3", 34 | "wrangler": "3.114.15" 35 | }, 36 | "@parcel/transformer-css": { 37 | "drafts": { 38 | "nesting": true 39 | } 40 | }, 41 | "size-limit": [ 42 | { 43 | "path": "src/_worker.ts", 44 | "brotli": false 45 | } 46 | ], 47 | "targets": { 48 | "frontend": { 49 | "source": [ 50 | "src/index.html", 51 | "src/404.html" 52 | ], 53 | "distDir": "dist/" 54 | }, 55 | "main": { 56 | "source": "src/_worker.ts", 57 | "context": "web-worker", 58 | "outputFormat": "esmodule", 59 | "includeNodeModules": true, 60 | "optimize": true 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | @import 'npm:ress'; 2 | @import 'npm:nord/src/nord.css'; 3 | 4 | body { 5 | font-family: -apple-system, BlinkMacSystemFont, system-ui, Roboto, 6 | 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 7 | 'Segoe UI Emoji', 'Segoe UI Symbol'; 8 | background: var(--nord0); 9 | overflow: hidden; 10 | } 11 | 12 | h1 { 13 | font-size: 22px; 14 | line-height: 24px; 15 | margin-bottom: 12px; 16 | } 17 | 18 | .main { 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | height: 100vh; 24 | width: 100vw; 25 | } 26 | 27 | .card { 28 | display: flex; 29 | flex-direction: column; 30 | width: 75%; 31 | max-width: 364px; 32 | padding: 24px; 33 | background: var(--nord4); 34 | color: var(--nord0); 35 | border-radius: 4px; 36 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 37 | overflow: hidden; 38 | } 39 | 40 | a { 41 | text-decoration-color: var(--nord10); 42 | color: var(--nord10); 43 | } 44 | 45 | hr { 46 | border: none; 47 | border-bottom: solid 1px var(--nord0); 48 | opacity: 0.12; 49 | margin: 4px 0 24px; 50 | } 51 | 52 | form { 53 | display: flex; 54 | height: 24px; 55 | #q { 56 | flex-grow: 1; 57 | outline-style: none; 58 | } 59 | button { 60 | height: 24px; 61 | width: 24px; 62 | &:hover, &:focus { 63 | background: var(--nord1); 64 | color: var(--nord4); 65 | } 66 | } 67 | &::before { 68 | content: ''; 69 | width: 100%; 70 | margin-right: -100%; 71 | border-bottom: solid 1px var(--nord0); 72 | } 73 | } 74 | 75 | .input { 76 | display: flex; 77 | flex-direction: column; 78 | &::after { 79 | content: ''; 80 | border-top: solid 2px var(--nord10); 81 | margin-top: -1.5px; 82 | align-self: center; 83 | width: 0; 84 | transition-timing-function: cubic-bezier(0.4, 0.0, 0.2, 1); 85 | @media not (prefers-reduced-motion) { 86 | transition: width 0.2s; 87 | } 88 | } 89 | &:has(form > #q:focus)::after { 90 | width: 100%; 91 | } 92 | margin-bottom: 24px; 93 | } 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node ### 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # Snowpack dependency directory (https://snowpack.dev/) 47 | web_modules/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Optional stylelint cache 59 | .stylelintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variable files 77 | .env 78 | .env.development.local 79 | .env.test.local 80 | .env.production.local 81 | .env.local 82 | 83 | # parcel-bundler cache (https://parceljs.org/) 84 | .cache 85 | .parcel-cache 86 | 87 | # Next.js build output 88 | .next 89 | out 90 | 91 | # Nuxt.js build / generate output 92 | .nuxt 93 | dist 94 | 95 | # Gatsby files 96 | .cache/ 97 | # Comment in the public line in if your project uses Gatsby and not Next.js 98 | # https://nextjs.org/blog/next-9-1#public-directory-support 99 | # public 100 | 101 | # vuepress build output 102 | .vuepress/dist 103 | 104 | # vuepress v2.x temp and cache directory 105 | .temp 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | ### Node Patch ### 133 | # Serverless Webpack directories 134 | .webpack/ 135 | 136 | # Optional stylelint cache 137 | 138 | # SvelteKit build / generate output 139 | .svelte-kit 140 | 141 | .wrangler 142 | -------------------------------------------------------------------------------- /src/routes/github.com.ts: -------------------------------------------------------------------------------- 1 | import {Router} from 'worktop'; 2 | import {reply} from 'worktop/response'; 3 | import forbidRepo from '../filter/repo'; 4 | import forbidUser from '../filter/user'; 5 | import {type ArchiveFormat, codeload} from './codeload.github.com'; 6 | import {raw} from './raw.githubusercontent.com'; 7 | 8 | const app = new Router(); 9 | 10 | // Tag names can include slashes, bailing out. 11 | app.add('GET', '/:user/:repo/releases/download/*', async (request, context) => { 12 | const {user, repo, '*': wild} = context.params; 13 | if (forbidUser(user) || forbidRepo({user, repo})) { 14 | return reply(403); 15 | } 16 | 17 | const range = request.headers.get('content-range'); 18 | const headers = new Headers(); 19 | if (range !== null) { 20 | headers.set('content-range', range); 21 | } 22 | 23 | return fetch( 24 | `https://github.com/${user}/${repo}/releases/download/${wild}`, 25 | { 26 | redirect: 'follow', 27 | headers: range === null ? headers : undefined, 28 | }, 29 | ); 30 | }); 31 | 32 | // GitHub uses this to prevent confusion between a tag named `latest` and the latest release 33 | app.add('GET', '/:user/:repo/releases/latest/download/:artifact', async (request, context) => { 34 | const {user, repo, artifact} = context.params; 35 | if (forbidUser(user) || forbidRepo({user, repo})) { 36 | return reply(403); 37 | } 38 | 39 | const range = request.headers.get('content-range'); 40 | const headers = new Headers(); 41 | if (range !== null) { 42 | headers.set('content-range', range); 43 | } 44 | 45 | return fetch( 46 | `https://github.com/${user}/${repo}/releases/latest/download/${artifact}`, 47 | { 48 | redirect: 'follow', 49 | headers: range === null ? headers : undefined, 50 | }, 51 | ); 52 | }); 53 | 54 | app.add('GET', '/:user/:repo/archive/*', async (_, context) => { 55 | const {user, repo, '*': wild} = context.params; 56 | if (forbidUser(user) || forbidRepo({user, repo})) { 57 | return reply(403); 58 | } 59 | 60 | let format: ArchiveFormat; 61 | if (wild.endsWith('.tar.gz')) { 62 | format = 'tar.gz'; 63 | } else if (wild.endsWith('.zip')) { 64 | format = 'zip'; 65 | } else { 66 | return reply(404, 'Not Found'); 67 | } 68 | 69 | // eslint-disable-next-line no-bitwise 70 | const reference = wild.slice(0, ~format.length); 71 | 72 | return codeload(user, repo, format, reference); 73 | }); 74 | 75 | app.add('GET', '/:user/:repo/zipball/*', async (_, context) => { 76 | const {user, repo, '*': reference} = context.params; 77 | if (forbidUser(user) || forbidRepo({user, repo})) { 78 | return reply(403); 79 | } 80 | 81 | return codeload(user, repo, 'legacy.zip', reference); 82 | }); 83 | 84 | app.add('GET', '/:user/:repo/tarball/*', async (_, context) => { 85 | const {user, repo, '*': reference} = context.params; 86 | if (forbidUser(user) || forbidRepo({user, repo})) { 87 | return reply(403); 88 | } 89 | 90 | return codeload(user, repo, 'legacy.tar.gz', reference); 91 | }); 92 | 93 | app.add('GET', '/:user/:repo/raw/*', async (_, context) => { 94 | const {user, repo, '*': path} = context.params; 95 | if (forbidUser(user) || forbidRepo({user, repo})) { 96 | return reply(403); 97 | } 98 | 99 | return raw(user, repo, path); 100 | }); 101 | 102 | app.add('GET', '/:user/:repo/info/refs', async (request, context) => { 103 | const service = context.url.searchParams.get('service'); 104 | if (service === null) { 105 | return reply(403, `Please upgrade your git client. 106 | GitHub.com no longer supports git over dumb-http: https://github.com/blog/809`); 107 | } 108 | 109 | if (service !== 'git-upload-pack') { 110 | return reply(404, 'Not Found'); 111 | } 112 | 113 | const {user, repo} = context.params; 114 | if (forbidUser(user) || forbidRepo({user, repo})) { 115 | return reply(403); 116 | } 117 | 118 | const headers = new Headers(); 119 | const protocol = request.headers.get('git-protocol'); 120 | 121 | if (protocol !== null) { 122 | headers.set('Git-Protocol', protocol); 123 | } 124 | 125 | return fetch(`https://github.com/${user}/${repo}/info/refs?service=git-upload-pack`, { 126 | headers, 127 | }); 128 | }); 129 | 130 | app.add('POST', '/:user/:repo/git-upload-pack', async (request, context) => { 131 | const {user, repo} = context.params; 132 | if (forbidUser(user) || forbidRepo({user, repo})) { 133 | return reply(403); 134 | } 135 | 136 | const headers = new Headers(); 137 | const protocol = request.headers.get('git-protocol'); 138 | const type = request.headers.get('content-type'); 139 | const encoding = request.headers.get('content-encoding'); 140 | 141 | if (protocol !== null) { 142 | headers.set('Git-Protocol', protocol); 143 | } 144 | 145 | if (type !== null) { 146 | headers.set('Content-Type', type); 147 | } 148 | 149 | if (encoding !== null) { 150 | headers.set('Content-Encoding', encoding); 151 | } 152 | 153 | return fetch(`https://github.com/${user}/${repo}/git-upload-pack`, { 154 | method: 'POST', 155 | body: request.body, 156 | headers, 157 | }); 158 | }); 159 | 160 | export default app; 161 | --------------------------------------------------------------------------------