From bdab7ddd67d8e447451771c7db4d7523a0b44129 Mon Sep 17 00:00:00 2001 From: hunshcn Date: Sat, 29 Apr 2023 08:58:33 +0800 Subject: [PATCH] custom init Signed-off-by: hunshcn --- LICENSE | 21 ++++++++ go.mod | 3 ++ go.sum | 0 index.js | 109 +++++++++++++++++++++++++++++++++++++++++ main.go | 144 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 277 insertions(+) create mode 100644 LICENSE create mode 100644 go.mod create mode 100644 go.sum create mode 100644 index.js create mode 100644 main.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c813a85 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..32592d9 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module awesomeProject + +go 1.18 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/index.js b/index.js new file mode 100644 index 0000000..d38a7b4 --- /dev/null +++ b/index.js @@ -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} 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, + }) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..93a0114 --- /dev/null +++ b/main.go @@ -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)) +}