github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/internal/agent/http/proxy.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: 2021-Present The Jackal Authors 3 4 // Package http provides a http server for the webhook and proxy. 5 package http 6 7 import ( 8 "crypto/tls" 9 "fmt" 10 "io" 11 "net/http" 12 "net/http/httputil" 13 "net/url" 14 "strings" 15 16 "github.com/Racer159/jackal/src/config/lang" 17 "github.com/Racer159/jackal/src/internal/agent/state" 18 "github.com/Racer159/jackal/src/pkg/message" 19 "github.com/Racer159/jackal/src/pkg/transform" 20 ) 21 22 // ProxyHandler constructs a new httputil.ReverseProxy and returns an http handler. 23 func ProxyHandler() http.HandlerFunc { 24 return func(w http.ResponseWriter, r *http.Request) { 25 err := proxyRequestTransform(r) 26 if err != nil { 27 message.Debugf("%#v", err) 28 w.WriteHeader(http.StatusInternalServerError) 29 w.Write([]byte(lang.AgentErrUnableTransform)) 30 return 31 } 32 33 proxy := &httputil.ReverseProxy{Director: func(_ *http.Request) {}, ModifyResponse: proxyResponseTransform} 34 proxy.ServeHTTP(w, r) 35 } 36 } 37 38 func proxyRequestTransform(r *http.Request) error { 39 message.Debugf("Before Req %#v", r) 40 message.Debugf("Before Req URL %#v", r.URL) 41 42 // We add this so that we can use it to rewrite urls in the response if needed 43 r.Header.Add("X-Forwarded-Host", r.Host) 44 45 // We remove this so that go will encode and decode on our behalf (see https://pkg.go.dev/net/http#Transport DisableCompression) 46 r.Header.Del("Accept-Encoding") 47 48 jackalState, err := state.GetJackalStateFromAgentPod() 49 if err != nil { 50 return err 51 } 52 53 var targetURL *url.URL 54 55 // Setup authentication for each type of service based on User Agent 56 switch { 57 case isGitUserAgent(r.UserAgent()): 58 r.SetBasicAuth(jackalState.GitServer.PushUsername, jackalState.GitServer.PushPassword) 59 case isNpmUserAgent(r.UserAgent()): 60 r.Header.Set("Authorization", "Bearer "+jackalState.ArtifactServer.PushToken) 61 default: 62 r.SetBasicAuth(jackalState.ArtifactServer.PushUsername, jackalState.ArtifactServer.PushToken) 63 } 64 65 // Transform the URL; if we see the NoTransform prefix, strip it; otherwise, transform the URL based on User Agent 66 if strings.HasPrefix(r.URL.Path, transform.NoTransform) { 67 switch { 68 case isGitUserAgent(r.UserAgent()): 69 targetURL, err = transform.NoTransformTarget(jackalState.GitServer.Address, r.URL.Path) 70 default: 71 targetURL, err = transform.NoTransformTarget(jackalState.ArtifactServer.Address, r.URL.Path) 72 } 73 } else { 74 switch { 75 case isGitUserAgent(r.UserAgent()): 76 targetURL, err = transform.GitURL(jackalState.GitServer.Address, getTLSScheme(r.TLS)+r.Host+r.URL.String(), jackalState.GitServer.PushUsername) 77 case isPipUserAgent(r.UserAgent()): 78 targetURL, err = transform.PipTransformURL(jackalState.ArtifactServer.Address, getTLSScheme(r.TLS)+r.Host+r.URL.String()) 79 case isNpmUserAgent(r.UserAgent()): 80 targetURL, err = transform.NpmTransformURL(jackalState.ArtifactServer.Address, getTLSScheme(r.TLS)+r.Host+r.URL.String()) 81 default: 82 targetURL, err = transform.GenTransformURL(jackalState.ArtifactServer.Address, getTLSScheme(r.TLS)+r.Host+r.URL.String()) 83 } 84 } 85 86 if err != nil { 87 return err 88 } 89 90 r.Host = targetURL.Host 91 r.URL = targetURL 92 r.RequestURI = getRequestURI(targetURL.Path, targetURL.RawQuery, targetURL.Fragment) 93 94 message.Debugf("After Req %#v", r) 95 message.Debugf("After Req URL%#v", r.URL) 96 97 return nil 98 } 99 100 func proxyResponseTransform(resp *http.Response) error { 101 message.Debugf("Before Resp %#v", resp) 102 103 // Handle redirection codes (3xx) by adding a marker to let Jackal know this has been redirected 104 if resp.StatusCode/100 == 3 { 105 message.Debugf("Before Resp Location %#v", resp.Header.Get("Location")) 106 107 locationURL, err := url.Parse(resp.Header.Get("Location")) 108 message.Debugf("%#v", err) 109 locationURL.Path = transform.NoTransform + locationURL.Path 110 locationURL.Host = resp.Request.Header.Get("X-Forwarded-Host") 111 112 resp.Header.Set("Location", locationURL.String()) 113 114 message.Debugf("After Resp Location %#v", resp.Header.Get("Location")) 115 } 116 117 contentType := resp.Header.Get("Content-Type") 118 119 // Handle text content returns that may contain links 120 if strings.HasPrefix(contentType, "text") || strings.HasPrefix(contentType, "application/json") || strings.HasPrefix(contentType, "application/xml") { 121 err := replaceBodyLinks(resp) 122 123 if err != nil { 124 message.Debugf("%#v", err) 125 } 126 } 127 128 message.Debugf("After Resp %#v", resp) 129 130 return nil 131 } 132 133 func replaceBodyLinks(resp *http.Response) error { 134 message.Debugf("Resp Request: %#v", resp.Request) 135 136 // Create the forwarded (online) and target (offline) URL prefixes to replace 137 forwardedPrefix := fmt.Sprintf("%s%s%s", getTLSScheme(resp.Request.TLS), resp.Request.Header.Get("X-Forwarded-Host"), transform.NoTransform) 138 targetPrefix := fmt.Sprintf("%s%s", getTLSScheme(resp.TLS), resp.Request.Host) 139 140 body, err := io.ReadAll(resp.Body) 141 if err != nil { 142 return err 143 } 144 145 err = resp.Body.Close() 146 if err != nil { 147 return err 148 } 149 150 bodyString := string(body) 151 message.Warnf("%s", bodyString) 152 153 bodyString = strings.ReplaceAll(bodyString, targetPrefix, forwardedPrefix) 154 155 message.Warnf("%s", bodyString) 156 157 // Setup the new reader, and correct the content length 158 resp.Body = io.NopCloser(strings.NewReader(bodyString)) 159 resp.ContentLength = int64(len(bodyString)) 160 resp.Header.Set("Content-Length", fmt.Sprint(int64(len(bodyString)))) 161 162 return nil 163 } 164 165 func getTLSScheme(tls *tls.ConnectionState) string { 166 scheme := "https://" 167 168 if tls == nil { 169 scheme = "http://" 170 } 171 172 return scheme 173 } 174 175 func getRequestURI(path, query, fragment string) string { 176 uri := path 177 178 if query != "" { 179 uri += "?" + query 180 } 181 182 if fragment != "" { 183 uri += "#" + fragment 184 } 185 186 return uri 187 } 188 189 func isGitUserAgent(userAgent string) bool { 190 return strings.HasPrefix(userAgent, "git") 191 } 192 193 func isPipUserAgent(userAgent string) bool { 194 return strings.HasPrefix(userAgent, "pip") || strings.HasPrefix(userAgent, "twine") 195 } 196 197 func isNpmUserAgent(userAgent string) bool { 198 return strings.HasPrefix(userAgent, "npm") || strings.HasPrefix(userAgent, "pnpm") || strings.HasPrefix(userAgent, "yarn") || strings.HasPrefix(userAgent, "bun") 199 }