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  }