mirror of https://github.com/hunshcn/gh-proxy
				
				
				
			
						commit
						bdab7ddd67
					
				|  | @ -0,0 +1,21 @@ | ||||||
|  | MIT License | ||||||
|  | 
 | ||||||
|  | Copyright (c) 2020 hunshcn | ||||||
|  | 
 | ||||||
|  | Permission is hereby granted, free of charge, to any person obtaining a copy | ||||||
|  | of this software and associated documentation files (the "Software"), to deal | ||||||
|  | in the Software without restriction, including without limitation the rights | ||||||
|  | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||||
|  | copies of the Software, and to permit persons to whom the Software is | ||||||
|  | furnished to do so, subject to the following conditions: | ||||||
|  | 
 | ||||||
|  | The above copyright notice and this permission notice shall be included in all | ||||||
|  | copies or substantial portions of the Software. | ||||||
|  | 
 | ||||||
|  | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||||
|  | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||||
|  | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||||
|  | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||||
|  | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||||
|  | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||||
|  | SOFTWARE. | ||||||
|  | @ -0,0 +1,109 @@ | ||||||
|  | 'use strict' | ||||||
|  | 
 | ||||||
|  | /** @type {RequestInit} */ | ||||||
|  | const PREFLIGHT_INIT = { | ||||||
|  |     status: 204, | ||||||
|  |     headers: new Headers({ | ||||||
|  |         'access-control-allow-origin': '*', | ||||||
|  |         'access-control-allow-methods': 'GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS', | ||||||
|  |         'access-control-max-age': '1728000', | ||||||
|  |     }), | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const PREFIX = 'https://example.com/' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @param {any} body | ||||||
|  |  * @param {number} status | ||||||
|  |  * @param {Object<string, string>} headers | ||||||
|  |  */ | ||||||
|  | function makeRes(body, status = 200, headers = {}) { | ||||||
|  |     headers['access-control-allow-origin'] = '*' | ||||||
|  |     return new Response(body, {status, headers}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | addEventListener('fetch', e => { | ||||||
|  |     const ret = fetchHandler(e) | ||||||
|  |         .catch(err => makeRes('cfworker error:\n' + err.stack, 502)) | ||||||
|  |     e.respondWith(ret) | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @param {FetchEvent} e | ||||||
|  |  */ | ||||||
|  | async function fetchHandler(e) { | ||||||
|  |     const req = e.request | ||||||
|  |     const urlStr = req.url | ||||||
|  |     const urlObj = new URL(urlStr) | ||||||
|  |     let path = urlStr.substring(urlObj.origin.length + 1).replace(/^(https?):\/+/, '$1://') | ||||||
|  |     return httpHandler(req, path) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @param {Request} req | ||||||
|  |  * @param {string} pathname | ||||||
|  |  */ | ||||||
|  | function httpHandler(req, pathname) { | ||||||
|  |     const reqHdrRaw = req.headers | ||||||
|  | 
 | ||||||
|  |     // preflight
 | ||||||
|  |     if (req.method === 'OPTIONS' && | ||||||
|  |         reqHdrRaw.has('access-control-request-headers') | ||||||
|  |     ) { | ||||||
|  |         return new Response(null, PREFLIGHT_INIT) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const reqHdrNew = new Headers(reqHdrRaw) | ||||||
|  | 
 | ||||||
|  |     /** @type {RequestInit} */ | ||||||
|  |     const reqInit = { | ||||||
|  |         method: req.method, | ||||||
|  |         headers: reqHdrNew, | ||||||
|  |         redirect: 'manual', | ||||||
|  |         body: req.body | ||||||
|  |     } | ||||||
|  |     return proxy(pathname, reqInit) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * | ||||||
|  |  * @param {string} path | ||||||
|  |  * @param {RequestInit} reqInit | ||||||
|  |  */ | ||||||
|  | async function proxy(path, reqInit) { | ||||||
|  |     const res = await fetch(path, reqInit) | ||||||
|  |     const resHdrOld = res.headers | ||||||
|  |     const resHdrNew = new Headers(resHdrOld) | ||||||
|  | 
 | ||||||
|  |     const status = res.status | ||||||
|  | 
 | ||||||
|  |     if (resHdrNew.has('location')) { | ||||||
|  |         let _location = resHdrNew.get('location') | ||||||
|  |         resHdrNew.set('location', PREFIX + _location) | ||||||
|  |     } | ||||||
|  |     resHdrNew.set('access-control-expose-headers', '*') | ||||||
|  |     resHdrNew.set('access-control-allow-origin', '*') | ||||||
|  | 
 | ||||||
|  |     resHdrNew.delete('content-security-policy') | ||||||
|  |     resHdrNew.delete('content-security-policy-report-only') | ||||||
|  |     resHdrNew.delete('clear-site-data') | ||||||
|  |     if (path.substring(0, path.lastIndexOf("/")).endsWith('/info/lfs/objects')) { | ||||||
|  |         let text = await res.text() | ||||||
|  |         text = text.replaceAll('"href":"https://', '"href":"' + PREFIX + 'https://') | ||||||
|  |         resHdrNew.delete("content-length") | ||||||
|  |         return new Response(text, { | ||||||
|  |             status, | ||||||
|  |             headers: resHdrNew, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return new Response(res.body, { | ||||||
|  |         status, | ||||||
|  |         headers: resHdrNew, | ||||||
|  |     }) | ||||||
|  | } | ||||||
|  | @ -0,0 +1,144 @@ | ||||||
|  | package main | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/http/httputil" | ||||||
|  | 	"net/url" | ||||||
|  | 	"sort" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const HostPrefix = "http://127.0.0.1:8080/" | ||||||
|  | 
 | ||||||
|  | func NewServer(enableLog bool) *Server { | ||||||
|  | 	director := func(req *http.Request) { | ||||||
|  | 		if _, ok := req.Header["User-Agent"]; !ok { | ||||||
|  | 			// explicitly disable User-Agent so it's not set to default value
 | ||||||
|  | 			req.Header.Set("User-Agent", "") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return &Server{proxy: &httputil.ReverseProxy{Director: director}, enableLog: enableLog} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type Server struct { | ||||||
|  | 	proxy     *httputil.ReverseProxy | ||||||
|  | 	enableLog bool | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	targetUrl := r.URL.RequestURI() | ||||||
|  | 	targetUrl = targetUrl[1:] | ||||||
|  | 	target, err := url.Parse(targetUrl) | ||||||
|  | 	if err != nil { | ||||||
|  | 		w.WriteHeader(500) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	r.Host = target.Host | ||||||
|  | 	r.URL = target | ||||||
|  | 	r.URL.Host = target.Host | ||||||
|  | 	r.URL.Path = target.Path | ||||||
|  | 	r.URL.RawPath = target.RawPath | ||||||
|  | 	mw := &MWriter{Next: s.proxy.ServeHTTP, EnableLog: s.enableLog} | ||||||
|  | 
 | ||||||
|  | 	if strings.HasSuffix(targetUrl[:strings.LastIndexByte(targetUrl, '/')], "/info/lfs/objects") { | ||||||
|  | 		mw.ReplaceFunc = func(mw *MWriter) { | ||||||
|  | 			for k, v := range mw.cache.header { | ||||||
|  | 				if k == "Content-Length" { | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 				mw.writer.Header()[k] = v | ||||||
|  | 			} | ||||||
|  | 			if mw.cache.statusCode != 0 { | ||||||
|  | 				mw.writer.WriteHeader(mw.cache.statusCode) | ||||||
|  | 			} | ||||||
|  | 			content := mw.cache.body.String() | ||||||
|  | 			content = strings.ReplaceAll(content, "\"href\":\"https://", "\"href\":\""+HostPrefix+"https://") | ||||||
|  | 			_, _ = mw.writer.Write([]byte(content)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	mw.ServeHTTP(w, r) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type MWriter struct { | ||||||
|  | 	writer  http.ResponseWriter | ||||||
|  | 	request *http.Request | ||||||
|  | 	cache   struct { | ||||||
|  | 		header     http.Header | ||||||
|  | 		body       bytes.Buffer | ||||||
|  | 		statusCode int | ||||||
|  | 	} | ||||||
|  | 	wroteLog bool | ||||||
|  | 
 | ||||||
|  | 	Next        func(http.ResponseWriter, *http.Request) | ||||||
|  | 	ReplaceFunc func(*MWriter) | ||||||
|  | 	EnableLog   bool | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (mw *MWriter) Log() { | ||||||
|  | 	if mw.EnableLog && !mw.wroteLog { | ||||||
|  | 		if mw.cache.statusCode == 0 { | ||||||
|  | 			mw.cache.statusCode = http.StatusOK | ||||||
|  | 		} | ||||||
|  | 		fmt.Println(mw.request.Method, mw.request.URL.String(), mw.cache.statusCode) | ||||||
|  | 		header := mw.writer.Header() | ||||||
|  | 		if mw.ReplaceFunc != nil { | ||||||
|  | 			header = mw.cache.header | ||||||
|  | 		} | ||||||
|  | 		var keys []string | ||||||
|  | 		for key := range header { | ||||||
|  | 			keys = append(keys, key) | ||||||
|  | 		} | ||||||
|  | 		sort.Strings(keys) | ||||||
|  | 		for _, key := range keys { | ||||||
|  | 			for _, value := range header[key] { | ||||||
|  | 				fmt.Printf("%s: %s\n", key, value) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		fmt.Println() | ||||||
|  | 		mw.wroteLog = true | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (mw *MWriter) Write(data []byte) (int, error) { | ||||||
|  | 	mw.Log() | ||||||
|  | 	if mw.ReplaceFunc == nil { | ||||||
|  | 		return mw.writer.Write(data) | ||||||
|  | 	} | ||||||
|  | 	return mw.cache.body.Write(data) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (mw *MWriter) WriteHeader(statusCode int) { | ||||||
|  | 	for i := range mw.Header()["Location"] { | ||||||
|  | 		mw.Header()["Location"][i] = HostPrefix + mw.Header()["Location"][0] | ||||||
|  | 	} | ||||||
|  | 	mw.Log() | ||||||
|  | 	if mw.ReplaceFunc == nil { | ||||||
|  | 		mw.writer.WriteHeader(statusCode) | ||||||
|  | 	} | ||||||
|  | 	mw.cache.statusCode = statusCode | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (mw *MWriter) Header() http.Header { | ||||||
|  | 	if mw.ReplaceFunc == nil { | ||||||
|  | 		return mw.writer.Header() | ||||||
|  | 	} | ||||||
|  | 	if mw.cache.header == nil { | ||||||
|  | 		mw.cache.header = mw.writer.Header().Clone() | ||||||
|  | 	} | ||||||
|  | 	return mw.cache.header | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (mw *MWriter) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	mw.writer = w | ||||||
|  | 	mw.request = r | ||||||
|  | 	mw.Next(mw, r) | ||||||
|  | 	if mw.ReplaceFunc != nil { | ||||||
|  | 		mw.ReplaceFunc(mw) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func main() { | ||||||
|  | 	http.ListenAndServe(":8080", NewServer(true)) | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue
	
	 hunshcn
						hunshcn