├── .gitignore ├── LICENSE ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 hunshcn 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gh-proxy 2 | 3 | ## 简介 4 | 基于项目: 5 | - [https://github.com/hunshcn/gh-proxy](https://github.com/hunshcn/gh-proxy) 6 | 7 | > 基于的是早期版本,可能和源项目有些不一样。 8 | 9 | 更改内容: 10 | - 移除Docker部署、Python部署,只保留Cloudfare Worker部署 11 | - 修改为可自定义镜像地址,方便自定义 12 | 13 | 14 | ## 演示 15 | 16 | [https://tool.mintimate.cn/gh](https://tool.mintimate.cn/gh) 17 | 18 | 演示站为公共服务,如有大规模使用需求请自行部署,演示站有点不堪重负 19 | 20 | ![imagea272c95887343279.png](https://img.maocdn.cn/img/2021/04/24/imagea272c95887343279.png) 21 | 22 | 23 | ## 使用 24 | 25 | 直接在copy出来的url前加`https://tool.mintimate.cn/gh/`即可。 26 | 27 | 如果是自己搭建的,一般为`https://**.workers.dev/`,将这个追加到需要加速的GitHub连接前即可。 28 | 29 | 30 | 以下都是合法输入(仅示例,文件不存在): 31 | 32 | - 分支源码:https://github.com/Mintimate/project/archive/master.zip 33 | 34 | - release源码:https://github.com/Mintimate/project/archive/v0.1.0.tar.gz 35 | 36 | - release文件:https://github.com/Mintimate/project/releases/download/v0.1.0/example.zip 37 | 38 | - 分支文件:https://github.com/Mintimate/project/blob/master/filename 39 | 40 | - commit文件:https://github.com/Mintimate/project/blob/1111111111111111111111111111/filename 41 | 42 | - gist:https://gist.githubusercontent.com/Mintimate/351557e6e465c12986419ac5a4dd2568/raw/cmd.py 43 | 44 | ## 部署 45 | 46 | 首页:https://workers.cloudflare.com 47 | 48 | 注册,登陆,`Start building`,取一个子域名,`Create a Worker`。 49 | 50 | 复制 [index.js](https://cdn.jsdelivr.net/gh/hunshcn/gh-proxy@master/index.js) 到左侧代码框,`Save and deploy`。如果正常,右侧应显示首页。 51 | 52 | `index.js`默认配置下项目文件会走jsDelivr,如需走worker,修改Config变量即可 53 | 54 | `ASSET_URL`是静态资源的url(实际上就是现在显示出来的那个输入框单页面) 55 | 56 | `PREFIX`是前缀,默认(根路径情况为"/"),如果自定义路由为example.com/gh/*,请将PREFIX改为 '/gh/',注意,少一个杠都会错! 57 | 58 | 59 | ## 注意 60 | 61 | python版本的机器如果无法正常访问github.io会启动报错,请自行修改静态文件url 62 | 63 | workers版本默认配置下项目文件会走jsDelivr,如需走服务器,修改配置即可 64 | 65 | 66 | ## Cloudflare Workers计费 67 | 68 | 到 `overview` 页面可参看使用情况。免费版每天有 10 万次免费请求,并且有每分钟1000次请求的限制。 69 | 70 | 如果不够用,可升级到 $5 的高级版本,每月可用 1000 万次请求(超出部分 $0.5/百万次请求)。 71 | 72 | ## Changelog 73 | 74 | * 2020.04.10 增加对`raw.githubusercontent.com`文件的支持 75 | * 2020.04.09 增加Python版本(使用Flask) 76 | * 2020.03.23 新增了clone的支持 77 | * 2020.03.22 初始版本 78 | 79 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author: Mintimate 3 | * Source: https://github.com/Mintimate/gh-proxy 4 | * Changed From: https://github.com/hunshcn/gh-proxy 5 | */ 6 | 7 | 'use strict' 8 | 9 | /** 10 | * static files (404.html, sw.js, conf.js) 11 | */ 12 | const ASSET_URL = 'https://tool.mintimate.cn' 13 | // const ASSET_URL = 'https://github.com' 14 | 15 | // 前缀,如果自定义路由为example.com/gh/*,将PREFIX改为 '/gh/',注意,少一个杠都会错! 16 | const PREFIX = '/gh/' 17 | 18 | /** 19 | * git clone镜像、文件分支文件镜像的开关 20 | * 0为关闭:使用Cloudfare 21 | * 1为开启:使用镜像 22 | * 默认为开启 23 | */ 24 | const Config = { 25 | staticFile: 1, 26 | cloneMirror: 1, 27 | } 28 | /** 29 | * 与上述配置对应 30 | * staticFileHost:静态文件镜像地址 31 | * cloneMirrorHost:git clone镜像地址 32 | */ 33 | const MirrorHost = { 34 | staticFileHost: 'https://fastly.jsdelivr.net/gh', 35 | cloneMirrorHost: 'https://hub.fastgit.xyz/', 36 | } 37 | 38 | const whiteList = [] // 白名单,路径里面有包含字符的才会通过,e.g. ['/username/'] 39 | 40 | /** @type {ResponseInit} */ 41 | const PREFLIGHT_INIT = { 42 | status: 204, 43 | headers: new Headers({ 44 | 'access-control-allow-origin': '*', 45 | 'access-control-allow-methods': 'GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS', 46 | 'access-control-max-age': '1728000', 47 | }), 48 | } 49 | 50 | 51 | const exp1 = /^(?:https?:\/\/)?github\.com\/.+?\/.+?\/(?:releases|archive)\/.*$/i 52 | const exp2 = /^(?:https?:\/\/)?github\.com\/.+?\/.+?\/(?:blob|raw)\/.*$/i 53 | const exp3 = /^(?:https?:\/\/)?github\.com\/.+?\/.+?\/(?:info|git-).*$/i 54 | const exp4 = /^(?:https?:\/\/)?raw\.(?:githubusercontent|github)\.com\/.+?\/.+?\/.+?\/.+$/i 55 | const exp5 = /^(?:https?:\/\/)?gist\.(?:githubusercontent|github)\.com\/.+?\/.+?\/.+$/i 56 | const exp6 = /^(?:https?:\/\/)?github\.com\/.+?\/.+?\/tags.*$/i 57 | 58 | /** 59 | * @param {any} body 60 | * @param {number} status 61 | * @param {Object} headers 62 | */ 63 | function makeRes(body, status = 200, headers = {}) { 64 | headers['access-control-allow-origin'] = '*' 65 | return new Response(body, {status, headers}) 66 | } 67 | 68 | 69 | /** 70 | * @param {string} urlStr 71 | */ 72 | function newUrl(urlStr) { 73 | try { 74 | return new URL(urlStr) 75 | } catch (err) { 76 | return null 77 | } 78 | } 79 | 80 | 81 | addEventListener('fetch', e => { 82 | const ret = fetchHandler(e) 83 | .catch(err => makeRes('cfworker error:\n' + err.stack, 502)) 84 | e.respondWith(ret) 85 | }) 86 | 87 | 88 | function checkUrl(u) { 89 | for (let i of [exp1, exp2, exp3, exp4, exp5, exp6]) { 90 | if (u.search(i) === 0) { 91 | return true 92 | } 93 | } 94 | return false 95 | } 96 | 97 | /** 98 | * @param {FetchEvent} e 99 | */ 100 | async function fetchHandler(e) { 101 | const req = e.request 102 | const urlStr = req.url 103 | const urlObj = new URL(urlStr) 104 | let path = urlObj.searchParams.get('q') 105 | if (path) { 106 | return Response.redirect('https://' + urlObj.host + PREFIX + path, 301) 107 | } 108 | // cfworker 会把路径中的 `//` 合并成 `/` 109 | path = urlObj.href.substr(urlObj.origin.length + PREFIX.length).replace(/^https?:\/+/, 'https://') 110 | if (path.search(exp1) === 0 || path.search(exp5) === 0 || path.search(exp6) === 0 || path.search(exp3) === 0 || path.search(exp4) === 0) { 111 | return httpHandler(req, path) 112 | } else if (path.search(exp2) === 0) { 113 | if (Config.staticFile){ 114 | const newUrl = path.replace('/blob/', '@').replace(/^(?:https?:\/\/)?github\.com/, MirrorHost.staticFileHost) 115 | return Response.redirect(newUrl, 302) 116 | } else { 117 | path = path.replace('/blob/', '/raw/') 118 | return httpHandler(req, path) 119 | } 120 | } else if (path.search(exp4) === 0) { 121 | const newUrl = path.replace(/(?<=com\/.+?\/.+?)\/(.+?\/)/, '@$1').replace(/^(?:https?:\/\/)?raw\.(?:githubusercontent|github)\.com/, MirrorHost.staticFileHost) 122 | return Response.redirect(newUrl, 302) 123 | } else { 124 | return fetch(ASSET_URL + path) 125 | } 126 | } 127 | 128 | 129 | /** 130 | * @param {Request} req 131 | * @param {string} pathname 132 | */ 133 | function httpHandler(req, pathname) { 134 | const reqHdrRaw = req.headers 135 | 136 | // preflight 137 | if (req.method === 'OPTIONS' && 138 | reqHdrRaw.has('access-control-request-headers') 139 | ) { 140 | return new Response(null, PREFLIGHT_INIT) 141 | } 142 | 143 | const reqHdrNew = new Headers(reqHdrRaw) 144 | 145 | let urlStr = pathname 146 | let flag = !Boolean(whiteList.length) 147 | for (let i of whiteList) { 148 | if (urlStr.includes(i)) { 149 | flag = true 150 | break 151 | } 152 | } 153 | if (!flag) { 154 | return new Response("blocked", {status: 403}) 155 | } 156 | if (urlStr.startsWith('github')) { 157 | urlStr = 'https://' + urlStr 158 | } 159 | const urlObj = newUrl(urlStr) 160 | 161 | /** @type {RequestInit} */ 162 | const reqInit = { 163 | method: req.method, 164 | headers: reqHdrNew, 165 | redirect: 'manual', 166 | body: req.body 167 | } 168 | return proxy(urlObj, reqInit) 169 | } 170 | 171 | 172 | /** 173 | * 174 | * @param {URL} urlObj 175 | * @param {RequestInit} reqInit 176 | */ 177 | async function proxy(urlObj, reqInit) { 178 | const res = await fetch(urlObj.href, reqInit) 179 | const resHdrOld = res.headers 180 | const resHdrNew = new Headers(resHdrOld) 181 | 182 | const status = res.status 183 | 184 | if (resHdrNew.has('location')) { 185 | let _location = resHdrNew.get('location') 186 | if (checkUrl(_location)) 187 | resHdrNew.set('location', PREFIX + _location) 188 | else { 189 | reqInit.redirect = 'follow' 190 | return proxy(newUrl(_location), reqInit) 191 | } 192 | } 193 | resHdrNew.set('access-control-expose-headers', '*') 194 | resHdrNew.set('access-control-allow-origin', '*') 195 | 196 | resHdrNew.delete('content-security-policy') 197 | resHdrNew.delete('content-security-policy-report-only') 198 | resHdrNew.delete('clear-site-data') 199 | 200 | return new Response(res.body, { 201 | status, 202 | headers: resHdrNew, 203 | }) 204 | } --------------------------------------------------------------------------------