├── Dockerfile ├── LICENSE ├── README.md ├── app ├── main.py └── uwsgi.ini ├── entrypoint.sh └── index.js /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM guysoft/uwsgi-nginx:python3.7 2 | 3 | LABEL maintainer="hunshcn " 4 | 5 | RUN pip install flask requests 6 | 7 | COPY ./app /app 8 | WORKDIR /app 9 | 10 | # Make /app/* available to be imported by Python globally to better support several use cases like Alembic migrations. 11 | ENV PYTHONPATH=/app 12 | 13 | # Move the base entrypoint to reuse it 14 | RUN mv /entrypoint.sh /uwsgi-nginx-entrypoint.sh 15 | # Copy the entrypoint that will generate Nginx additional configs 16 | COPY entrypoint.sh /entrypoint.sh 17 | RUN chmod +x /entrypoint.sh 18 | 19 | ENTRYPOINT ["/entrypoint.sh"] 20 | 21 | # Run the start script provided by the parent image tiangolo/uwsgi-nginx. 22 | # It will check for an /app/prestart.sh script (e.g. for migrations) 23 | # And then will start Supervisor, which in turn will start Nginx and uWSGI 24 | 25 | EXPOSE 80 26 | 27 | CMD ["/start.sh"] 28 | -------------------------------------------------------------------------------- /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 | github release、archive以及项目文件的加速项目,支持clone,有Cloudflare Workers无服务器版本以及Python版本 6 | 7 | ## 演示 8 | 9 | [https://gh.api.99988866.xyz/](https://gh.api.99988866.xyz/) 10 | 11 | 演示站为公共服务,如有大规模使用需求请自行部署,演示站有点不堪重负 12 | 13 | ![imagea272c95887343279.png](https://img.maocdn.cn/img/2021/04/24/imagea272c95887343279.png) 14 | 15 | 当然也欢迎[捐赠](#捐赠)以支持作者 16 | 17 | ## python版本和cf worker版本差异 18 | 19 | - python版本支持进行文件大小限制,超过设定返回原地址 [issue #8](https://github.com/hunshcn/gh-proxy/issues/8) 20 | 21 | - python版本支持特定user/repo 封禁/白名单 以及passby [issue #41](https://github.com/hunshcn/gh-proxy/issues/41) 22 | 23 | ## 使用 24 | 25 | 直接在copy出来的url前加`https://gh.api.99988866.xyz/`即可 26 | 27 | 也可以直接访问,在input输入 28 | 29 | ***大量使用请自行部署,以上域名仅为演示使用。*** 30 | 31 | 访问私有仓库可以通过 32 | 33 | `git clone https://user:TOKEN@ghproxy.com/https://github.com/xxxx/xxxx` [#71](https://github.com/hunshcn/gh-proxy/issues/71) 34 | 35 | 以下都是合法输入(仅示例,文件不存在): 36 | 37 | - 分支源码:https://github.com/hunshcn/project/archive/master.zip 38 | 39 | - release源码:https://github.com/hunshcn/project/archive/v0.1.0.tar.gz 40 | 41 | - release文件:https://github.com/hunshcn/project/releases/download/v0.1.0/example.zip 42 | 43 | - 分支文件:https://github.com/hunshcn/project/blob/master/filename 44 | 45 | - commit文件:https://github.com/hunshcn/project/blob/1111111111111111111111111111/filename 46 | 47 | - gist:https://gist.githubusercontent.com/cielpy/351557e6e465c12986419ac5a4dd2568/raw/cmd.py 48 | 49 | ## cf worker版本部署 50 | 51 | 首页:https://workers.cloudflare.com 52 | 53 | 注册,登陆,`Start building`,取一个子域名,`Create a Worker`。 54 | 55 | 复制 [index.js](https://cdn.jsdelivr.net/gh/hunshcn/gh-proxy@master/index.js) 到左侧代码框,`Save and deploy`。如果正常,右侧应显示首页。 56 | 57 | `ASSET_URL`是静态资源的url(实际上就是现在显示出来的那个输入框单页面) 58 | 59 | `PREFIX`是前缀,默认(根路径情况为"/"),如果自定义路由为example.com/gh/*,请将PREFIX改为 '/gh/',注意,少一个杠都会错! 60 | 61 | ## Python版本部署 62 | 63 | ### Docker部署 64 | 65 | ``` 66 | docker run -d --name="gh-proxy-py" \ 67 | -p 0.0.0.0:80:80 \ 68 | --restart=always \ 69 | hunsh/gh-proxy-py:latest 70 | ``` 71 | 72 | 第一个80是你要暴露出去的端口 73 | 74 | ### 直接部署 75 | 76 | 安装依赖(请使用python3) 77 | 78 | ```pip install flask requests``` 79 | 80 | 按需求修改`app/main.py`的前几项配置 81 | 82 | *注意:* 可能需要在`return Response`前加两行 83 | ```python3 84 | if 'Transfer-Encoding' in headers: 85 | headers.pop('Transfer-Encoding') 86 | ``` 87 | 88 | ### 注意 89 | 90 | python版本的机器如果无法正常访问github.io会启动报错,请自行修改静态文件url 91 | 92 | python版本默认走服务器(2021.3.27更新) 93 | 94 | ## Cloudflare Workers计费 95 | 96 | 到 `overview` 页面可参看使用情况。免费版每天有 10 万次免费请求,并且有每分钟1000次请求的限制。 97 | 98 | 如果不够用,可升级到 $5 的高级版本,每月可用 1000 万次请求(超出部分 $0.5/百万次请求)。 99 | 100 | ## Changelog 101 | 102 | * 2020.04.10 增加对`raw.githubusercontent.com`文件的支持 103 | * 2020.04.09 增加Python版本(使用Flask) 104 | * 2020.03.23 新增了clone的支持 105 | * 2020.03.22 初始版本 106 | 107 | ## 链接 108 | 109 | [我的博客](https://hunsh.net) 110 | 111 | ## 参考 112 | 113 | [jsproxy](https://github.com/EtherDream/jsproxy/) 114 | 115 | ## 捐赠 116 | 117 | ![wx.png](https://img.maocdn.cn/img/2021/04/24/image.md.png) 118 | ![ali.png](https://www.helloimg.com/images/2021/04/24/BK9vmb.md.png) 119 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | 4 | import requests 5 | from flask import Flask, Response, redirect, request 6 | from requests.exceptions import ( 7 | ChunkedEncodingError, 8 | ContentDecodingError, ConnectionError, StreamConsumedError) 9 | from requests.utils import ( 10 | stream_decode_response_unicode, iter_slices, CaseInsensitiveDict) 11 | from urllib3.exceptions import ( 12 | DecodeError, ReadTimeoutError, ProtocolError) 13 | from urllib.parse import quote 14 | 15 | # config 16 | # 分支文件使用jsDelivr镜像的开关,0为关闭,默认关闭 17 | jsdelivr = 0 18 | size_limit = 1024 * 1024 * 1024 * 999 # 允许的文件大小,默认999GB,相当于无限制了 https://github.com/hunshcn/gh-proxy/issues/8 19 | 20 | """ 21 | 先生效白名单再匹配黑名单,pass_list匹配到的会直接302到jsdelivr而忽略设置 22 | 生效顺序 白->黑->pass,可以前往https://github.com/hunshcn/gh-proxy/issues/41 查看示例 23 | 每个规则一行,可以封禁某个用户的所有仓库,也可以封禁某个用户的特定仓库,下方用黑名单示例,白名单同理 24 | user1 # 封禁user1的所有仓库 25 | user1/repo1 # 封禁user1的repo1 26 | */repo1 # 封禁所有叫做repo1的仓库 27 | """ 28 | white_list = ''' 29 | ''' 30 | black_list = ''' 31 | ''' 32 | pass_list = ''' 33 | ''' 34 | 35 | HOST = '127.0.0.1' # 监听地址,建议监听本地然后由web服务器反代 36 | PORT = 80 # 监听端口 37 | ASSET_URL = 'https://hunshcn.github.io/gh-proxy' # 主页 38 | 39 | white_list = [tuple([x.replace(' ', '') for x in i.split('/')]) for i in white_list.split('\n') if i] 40 | black_list = [tuple([x.replace(' ', '') for x in i.split('/')]) for i in black_list.split('\n') if i] 41 | pass_list = [tuple([x.replace(' ', '') for x in i.split('/')]) for i in pass_list.split('\n') if i] 42 | app = Flask(__name__) 43 | CHUNK_SIZE = 1024 * 10 44 | index_html = requests.get(ASSET_URL, timeout=10).text 45 | icon_r = requests.get(ASSET_URL + '/favicon.ico', timeout=10).content 46 | exp1 = re.compile(r'^(?:https?://)?github\.com/(?P.+?)/(?P.+?)/(?:releases|archive)/.*$') 47 | exp2 = re.compile(r'^(?:https?://)?github\.com/(?P.+?)/(?P.+?)/(?:blob|raw)/.*$') 48 | exp3 = re.compile(r'^(?:https?://)?github\.com/(?P.+?)/(?P.+?)/(?:info|git-).*$') 49 | exp4 = re.compile(r'^(?:https?://)?raw\.(?:githubusercontent|github)\.com/(?P.+?)/(?P.+?)/.+?/.+$') 50 | exp5 = re.compile(r'^(?:https?://)?gist\.(?:githubusercontent|github)\.com/(?P.+?)/.+?/.+$') 51 | 52 | requests.sessions.default_headers = lambda: CaseInsensitiveDict() 53 | 54 | 55 | @app.route('/') 56 | def index(): 57 | if 'q' in request.args: 58 | return redirect('/' + request.args.get('q')) 59 | return index_html 60 | 61 | 62 | @app.route('/favicon.ico') 63 | def icon(): 64 | return Response(icon_r, content_type='image/vnd.microsoft.icon') 65 | 66 | 67 | def iter_content(self, chunk_size=1, decode_unicode=False): 68 | """rewrite requests function, set decode_content with False""" 69 | 70 | def generate(): 71 | # Special case for urllib3. 72 | if hasattr(self.raw, 'stream'): 73 | try: 74 | for chunk in self.raw.stream(chunk_size, decode_content=False): 75 | yield chunk 76 | except ProtocolError as e: 77 | raise ChunkedEncodingError(e) 78 | except DecodeError as e: 79 | raise ContentDecodingError(e) 80 | except ReadTimeoutError as e: 81 | raise ConnectionError(e) 82 | else: 83 | # Standard file-like object. 84 | while True: 85 | chunk = self.raw.read(chunk_size) 86 | if not chunk: 87 | break 88 | yield chunk 89 | 90 | self._content_consumed = True 91 | 92 | if self._content_consumed and isinstance(self._content, bool): 93 | raise StreamConsumedError() 94 | elif chunk_size is not None and not isinstance(chunk_size, int): 95 | raise TypeError("chunk_size must be an int, it is instead a %s." % type(chunk_size)) 96 | # simulate reading small chunks of the content 97 | reused_chunks = iter_slices(self._content, chunk_size) 98 | 99 | stream_chunks = generate() 100 | 101 | chunks = reused_chunks if self._content_consumed else stream_chunks 102 | 103 | if decode_unicode: 104 | chunks = stream_decode_response_unicode(chunks, self) 105 | 106 | return chunks 107 | 108 | 109 | def check_url(u): 110 | for exp in (exp1, exp2, exp3, exp4, exp5): 111 | m = exp.match(u) 112 | if m: 113 | return m 114 | return False 115 | 116 | 117 | @app.route('/', methods=['GET', 'POST']) 118 | def handler(u): 119 | u = u if u.startswith('http') else 'https://' + u 120 | if u.rfind('://', 3, 9) == -1: 121 | u = u.replace('s:/', 's://', 1) # uwsgi会将//传递为/ 122 | pass_by = False 123 | m = check_url(u) 124 | if m: 125 | m = tuple(m.groups()) 126 | if white_list: 127 | for i in white_list: 128 | if m[:len(i)] == i or i[0] == '*' and len(m) == 2 and m[1] == i[1]: 129 | break 130 | else: 131 | return Response('Forbidden by white list.', status=403) 132 | for i in black_list: 133 | if m[:len(i)] == i or i[0] == '*' and len(m) == 2 and m[1] == i[1]: 134 | return Response('Forbidden by black list.', status=403) 135 | for i in pass_list: 136 | if m[:len(i)] == i or i[0] == '*' and len(m) == 2 and m[1] == i[1]: 137 | pass_by = True 138 | break 139 | else: 140 | return Response('Invalid input.', status=403) 141 | 142 | if (jsdelivr or pass_by) and exp2.match(u): 143 | u = u.replace('/blob/', '@', 1).replace('github.com', 'cdn.jsdelivr.net/gh', 1) 144 | return redirect(u) 145 | elif (jsdelivr or pass_by) and exp4.match(u): 146 | u = re.sub(r'(\.com/.*?/.+?)/(.+?/)', r'\1@\2', u, 1) 147 | _u = u.replace('raw.githubusercontent.com', 'cdn.jsdelivr.net/gh', 1) 148 | u = u.replace('raw.github.com', 'cdn.jsdelivr.net/gh', 1) if _u == u else _u 149 | return redirect(u) 150 | else: 151 | if exp2.match(u): 152 | u = u.replace('/blob/', '/raw/', 1) 153 | if pass_by: 154 | url = u + request.url.replace(request.base_url, '', 1) 155 | if url.startswith('https:/') and not url.startswith('https://'): 156 | url = 'https://' + url[7:] 157 | return redirect(url) 158 | u = quote(u, safe='/:') 159 | return proxy(u) 160 | 161 | 162 | def proxy(u, allow_redirects=False): 163 | headers = {} 164 | r_headers = dict(request.headers) 165 | if 'Host' in r_headers: 166 | r_headers.pop('Host') 167 | try: 168 | url = u + request.url.replace(request.base_url, '', 1) 169 | if url.startswith('https:/') and not url.startswith('https://'): 170 | url = 'https://' + url[7:] 171 | r = requests.request(method=request.method, url=url, data=request.data, headers=r_headers, stream=True, allow_redirects=allow_redirects) 172 | headers = dict(r.headers) 173 | 174 | if 'Content-length' in r.headers and int(r.headers['Content-length']) > size_limit: 175 | return redirect(u + request.url.replace(request.base_url, '', 1)) 176 | 177 | def generate(): 178 | for chunk in iter_content(r, chunk_size=CHUNK_SIZE): 179 | yield chunk 180 | 181 | if 'Location' in r.headers: 182 | _location = r.headers.get('Location') 183 | if check_url(_location): 184 | headers['Location'] = '/' + _location 185 | else: 186 | return proxy(_location, True) 187 | 188 | return Response(generate(), headers=headers, status=r.status_code) 189 | except Exception as e: 190 | headers['content-type'] = 'text/html; charset=UTF-8' 191 | return Response('server error ' + str(e), status=500, headers=headers) 192 | 193 | app.debug = True 194 | if __name__ == '__main__': 195 | app.run(host=HOST, port=PORT) 196 | -------------------------------------------------------------------------------- /app/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | module = main 3 | callable = app 4 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | set -e 3 | 4 | /uwsgi-nginx-entrypoint.sh 5 | 6 | # Get the listen port for Nginx, default to 80 7 | USE_LISTEN_PORT=${LISTEN_PORT:-80} 8 | 9 | if [ -f /app/nginx.conf ]; then 10 | cp /app/nginx.conf /etc/nginx/nginx.conf 11 | else 12 | content_server='server {\n' 13 | content_server=$content_server" listen ${USE_LISTEN_PORT};\n" 14 | content_server=$content_server' location / {\n' 15 | content_server=$content_server' try_files $uri @app;\n' 16 | content_server=$content_server' }\n' 17 | content_server=$content_server' location @app {\n' 18 | content_server=$content_server' include uwsgi_params;\n' 19 | content_server=$content_server' uwsgi_pass unix:///tmp/uwsgi.sock;\n' 20 | content_server=$content_server' uwsgi_buffer_size 256k;\n' 21 | content_server=$content_server' uwsgi_buffers 32 512k;\n' 22 | content_server=$content_server' uwsgi_busy_buffers_size 512k;\n' 23 | content_server=$content_server' }\n' 24 | content_server=$content_server'}\n' 25 | # Save generated server /etc/nginx/conf.d/nginx.conf 26 | printf "$content_server" > /etc/nginx/conf.d/nginx.conf 27 | fi 28 | 29 | exec "$@" 30 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * static files (404.html, sw.js, conf.js) 5 | */ 6 | const ASSET_URL = 'https://hunshcn.github.io/gh-proxy/' 7 | // 前缀,如果自定义路由为example.com/gh/*,将PREFIX改为 '/gh/',注意,少一个杠都会错! 8 | const PREFIX = '/' 9 | // 分支文件使用jsDelivr镜像的开关,0为关闭,默认关闭 10 | const Config = { 11 | jsdelivr: 0 12 | } 13 | 14 | const whiteList = [] // 白名单,路径里面有包含字符的才会通过,e.g. ['/username/'] 15 | 16 | /** @type {ResponseInit} */ 17 | const PREFLIGHT_INIT = { 18 | status: 204, 19 | headers: new Headers({ 20 | 'access-control-allow-origin': '*', 21 | 'access-control-allow-methods': 'GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS', 22 | 'access-control-max-age': '1728000', 23 | }), 24 | } 25 | 26 | 27 | const exp1 = /^(?:https?:\/\/)?github\.com\/.+?\/.+?\/(?:releases|archive)\/.*$/i 28 | const exp2 = /^(?:https?:\/\/)?github\.com\/.+?\/.+?\/(?:blob|raw)\/.*$/i 29 | const exp3 = /^(?:https?:\/\/)?github\.com\/.+?\/.+?\/(?:info|git-).*$/i 30 | const exp4 = /^(?:https?:\/\/)?raw\.(?:githubusercontent|github)\.com\/.+?\/.+?\/.+?\/.+$/i 31 | const exp5 = /^(?:https?:\/\/)?gist\.(?:githubusercontent|github)\.com\/.+?\/.+?\/.+$/i 32 | const exp6 = /^(?:https?:\/\/)?github\.com\/.+?\/.+?\/tags.*$/i 33 | 34 | /** 35 | * @param {any} body 36 | * @param {number} status 37 | * @param {Object} headers 38 | */ 39 | function makeRes(body, status = 200, headers = {}) { 40 | headers['access-control-allow-origin'] = '*' 41 | return new Response(body, {status, headers}) 42 | } 43 | 44 | 45 | /** 46 | * @param {string} urlStr 47 | */ 48 | function newUrl(urlStr) { 49 | try { 50 | return new URL(urlStr) 51 | } catch (err) { 52 | return null 53 | } 54 | } 55 | 56 | 57 | addEventListener('fetch', e => { 58 | const ret = fetchHandler(e) 59 | .catch(err => makeRes('cfworker error:\n' + err.stack, 502)) 60 | e.respondWith(ret) 61 | }) 62 | 63 | 64 | function checkUrl(u) { 65 | for (let i of [exp1, exp2, exp3, exp4, exp5, exp6]) { 66 | if (u.search(i) === 0) { 67 | return true 68 | } 69 | } 70 | return false 71 | } 72 | 73 | /** 74 | * @param {FetchEvent} e 75 | */ 76 | async function fetchHandler(e) { 77 | const req = e.request 78 | const urlStr = req.url 79 | const urlObj = new URL(urlStr) 80 | let path = urlObj.searchParams.get('q') 81 | if (path) { 82 | return Response.redirect('https://' + urlObj.host + PREFIX + path, 301) 83 | } 84 | // cfworker 会把路径中的 `//` 合并成 `/` 85 | path = urlObj.href.substr(urlObj.origin.length + PREFIX.length).replace(/^https?:\/+/, 'https://') 86 | if (path.search(exp1) === 0 || path.search(exp5) === 0 || path.search(exp6) === 0 || path.search(exp3) === 0 || path.search(exp4) === 0) { 87 | return httpHandler(req, path) 88 | } else if (path.search(exp2) === 0) { 89 | if (Config.jsdelivr) { 90 | const newUrl = path.replace('/blob/', '@').replace(/^(?:https?:\/\/)?github\.com/, 'https://cdn.jsdelivr.net/gh') 91 | return Response.redirect(newUrl, 302) 92 | } else { 93 | path = path.replace('/blob/', '/raw/') 94 | return httpHandler(req, path) 95 | } 96 | } else if (path.search(exp4) === 0) { 97 | const newUrl = path.replace(/(?<=com\/.+?\/.+?)\/(.+?\/)/, '@$1').replace(/^(?:https?:\/\/)?raw\.(?:githubusercontent|github)\.com/, 'https://cdn.jsdelivr.net/gh') 98 | return Response.redirect(newUrl, 302) 99 | } else { 100 | return fetch(ASSET_URL + path) 101 | } 102 | } 103 | 104 | 105 | /** 106 | * @param {Request} req 107 | * @param {string} pathname 108 | */ 109 | function httpHandler(req, pathname) { 110 | const reqHdrRaw = req.headers 111 | 112 | // preflight 113 | if (req.method === 'OPTIONS' && 114 | reqHdrRaw.has('access-control-request-headers') 115 | ) { 116 | return new Response(null, PREFLIGHT_INIT) 117 | } 118 | 119 | const reqHdrNew = new Headers(reqHdrRaw) 120 | 121 | let urlStr = pathname 122 | let flag = !Boolean(whiteList.length) 123 | for (let i of whiteList) { 124 | if (urlStr.includes(i)) { 125 | flag = true 126 | break 127 | } 128 | } 129 | if (!flag) { 130 | return new Response("blocked", {status: 403}) 131 | } 132 | if (urlStr.search(/^https?:\/\//) !== 0) { 133 | urlStr = 'https://' + urlStr 134 | } 135 | const urlObj = newUrl(urlStr) 136 | 137 | /** @type {RequestInit} */ 138 | const reqInit = { 139 | method: req.method, 140 | headers: reqHdrNew, 141 | redirect: 'manual', 142 | body: req.body 143 | } 144 | return proxy(urlObj, reqInit) 145 | } 146 | 147 | 148 | /** 149 | * 150 | * @param {URL} urlObj 151 | * @param {RequestInit} reqInit 152 | */ 153 | async function proxy(urlObj, reqInit) { 154 | const res = await fetch(urlObj.href, reqInit) 155 | const resHdrOld = res.headers 156 | const resHdrNew = new Headers(resHdrOld) 157 | 158 | const status = res.status 159 | 160 | if (resHdrNew.has('location')) { 161 | let _location = resHdrNew.get('location') 162 | if (checkUrl(_location)) 163 | resHdrNew.set('location', PREFIX + _location) 164 | else { 165 | reqInit.redirect = 'follow' 166 | return proxy(newUrl(_location), reqInit) 167 | } 168 | } 169 | resHdrNew.set('access-control-expose-headers', '*') 170 | resHdrNew.set('access-control-allow-origin', '*') 171 | 172 | resHdrNew.delete('content-security-policy') 173 | resHdrNew.delete('content-security-policy-report-only') 174 | resHdrNew.delete('clear-site-data') 175 | 176 | return new Response(res.body, { 177 | status, 178 | headers: resHdrNew, 179 | }) 180 | } 181 | 182 | --------------------------------------------------------------------------------