Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions git/config_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package git

import (
"bytes"
"crypto/tls"
"io"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"

"github.com/agentuity/proxykit/cache"
Expand Down Expand Up @@ -35,4 +41,126 @@ func TestDefaultConfig(t *testing.T) {
if cfg.MaxReceivePackRequestSize != DefaultMaxReceivePackRequestSize {
t.Fatalf("MaxReceivePackRequestSize = %d", cfg.MaxReceivePackRequestSize)
}
if cfg.UpstreamScheme != "" {
t.Fatalf("UpstreamScheme = %q, want empty", cfg.UpstreamScheme)
}
}

func TestConfigValidateUpstreamScheme(t *testing.T) {
for _, scheme := range []string{"", "http", "https"} {
t.Run(scheme, func(t *testing.T) {
cfg := DefaultConfig(t.TempDir())
cfg.UpstreamScheme = scheme
if err := cfg.Validate(); err != nil {
t.Fatalf("Validate() error = %v", err)
}
})
}

for _, scheme := range []string{"HTTP", "ftp", "https://"} {
t.Run("invalid_"+scheme, func(t *testing.T) {
cfg := DefaultConfig(t.TempDir())
cfg.UpstreamScheme = scheme
err := cfg.Validate()
if err == nil {
t.Fatal("Validate() error = nil")
}
if !strings.Contains(err.Error(), "git.UpstreamScheme") {
t.Fatalf("Validate() error = %q", err)
}
})
}
}

func TestNewRejectsInvalidUpstreamScheme(t *testing.T) {
cfg := DefaultConfig(t.TempDir())
cfg.UpstreamScheme = "ftp"

if _, err := New(cfg); err == nil {
t.Fatal("New() error = nil")
} else if !strings.Contains(err.Error(), "git.UpstreamScheme") {
t.Fatalf("New() error = %q", err)
}
}

func TestUpstreamSchemeForStreamingAndBufferedRequests(t *testing.T) {
tests := []struct {
name string
configured string
requestUsesTLS bool
want string
}{
{name: "default plain HTTP", want: "http"},
{name: "default TLS", requestUsesTLS: true, want: "https"},
{name: "force HTTP for plain HTTP", configured: "http", want: "http"},
{name: "force HTTP for TLS", configured: "http", requestUsesTLS: true, want: "http"},
{name: "force HTTPS for plain HTTP", configured: "https", want: "https"},
{name: "force HTTPS for TLS", configured: "https", requestUsesTLS: true, want: "https"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for _, buffered := range []bool{false, true} {
path := "streaming"
if buffered {
path = "buffered"
}
t.Run(path, func(t *testing.T) {
const body = "request body"
req := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "http://git.example/repo.git/git-upload-pack?service=git-upload-pack", bytes.NewBufferString(body))
req.RequestURI = req.URL.RequestURI()
if tt.requestUsesTLS {
req.TLS = &tls.ConnectionState{}
}

var gotURL string
var gotBody string
h := &Handler{
cfg: Config{UpstreamScheme: tt.configured},
client: &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
gotURL = r.URL.String()
requestBody, err := io.ReadAll(r.Body)
if err != nil {
t.Fatalf("read upstream request body: %v", err)
}
gotBody = string(requestBody)
return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader("ok")),
}, nil
})},
}

var resp *http.Response
var err error
if buffered {
resp, err = h.doUpstreamRequestWithBody(req, []byte(body))
} else {
resp, err = h.doUpstreamRequest(req)
}
if err != nil {
t.Fatalf("upstream request error = %v", err)
}
if err := resp.Body.Close(); err != nil {
t.Fatalf("close upstream response body: %v", err)
}

wantURL := tt.want + "://git.example/repo.git/git-upload-pack?service=git-upload-pack"
if gotURL != wantURL {
t.Fatalf("upstream URL = %q, want %q", gotURL, wantURL)
}
if gotBody != body {
t.Fatalf("upstream body = %q, want %q", gotBody, body)
}
})
}
})
}
}

type roundTripFunc func(*http.Request) (*http.Response, error)

func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
return f(r)
}
18 changes: 18 additions & 0 deletions git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ type Config struct {
// Default: empty (cache all detected Git hosts).
AllowedHosts []string

// UpstreamScheme overrides the scheme used for upstream Git requests.
// Valid values are "http", "https", and empty. When empty, the handler
// uses "https" for TLS requests and "http" for plain HTTP requests.
UpstreamScheme string

// Logger receives cache and proxy diagnostics. A console logger is used when nil.
Logger logger.Logger
}
Expand Down Expand Up @@ -182,6 +187,9 @@ func effectivePort(u *url.URL) string {
// New creates a new Git proxy Handler. Creates cache directories if they don't exist.
// Returns an error if cache initialization fails.
func New(cfg Config) (*Handler, error) {
if err := validateUpstreamScheme(cfg.UpstreamScheme); err != nil {
return nil, err
}
if cfg.Logger == nil {
cfg.Logger = logger.NewConsoleLogger()
}
Expand Down Expand Up @@ -353,6 +361,9 @@ func (cfg *Config) Validate() error {
if cfg.MaxPackCacheEntrySize < 0 {
return errors.New("git.MaxPackCacheEntrySize must not be negative")
}
if err := validateUpstreamScheme(cfg.UpstreamScheme); err != nil {
return err
}

for i, host := range cfg.AllowedHosts {
if strings.TrimSpace(host) == "" {
Expand All @@ -366,6 +377,13 @@ func (cfg *Config) Validate() error {
return nil
}

func validateUpstreamScheme(scheme string) error {
if scheme != "" && scheme != "http" && scheme != "https" {
return fmt.Errorf("git.UpstreamScheme must be empty, http, or https: %q", scheme)
}
return nil
}

// envVarNameRegexp validates environment variable names.
var envVarNameRegexp = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)

Expand Down
22 changes: 12 additions & 10 deletions git/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -531,11 +531,7 @@ func (h *Handler) forwardDirect(w http.ResponseWriter, r *http.Request, body []b
// doUpstreamRequest creates and sends an upstream request using the original
// request's headers (which already have real credentials injected by InjectSecrets).
func (h *Handler) doUpstreamRequest(r *http.Request) (*http.Response, error) {
scheme := "https"
if r.TLS == nil {
scheme = "http"
}
targetURL := fmt.Sprintf("%s://%s%s", scheme, r.Host, r.RequestURI)
targetURL := fmt.Sprintf("%s://%s%s", h.upstreamScheme(r), r.Host, r.RequestURI)

upstreamReq, err := http.NewRequestWithContext(r.Context(), r.Method, targetURL, r.Body)
if err != nil {
Expand All @@ -550,11 +546,7 @@ func (h *Handler) doUpstreamRequest(r *http.Request) (*http.Response, error) {
// doUpstreamRequestWithBody creates and sends an upstream request using a
// pre-buffered body (for upload-pack requests that were buffered for parsing).
func (h *Handler) doUpstreamRequestWithBody(r *http.Request, body []byte) (*http.Response, error) {
scheme := "https"
if r.TLS == nil {
scheme = "http"
}
targetURL := fmt.Sprintf("%s://%s%s", scheme, r.Host, r.RequestURI)
targetURL := fmt.Sprintf("%s://%s%s", h.upstreamScheme(r), r.Host, r.RequestURI)

upstreamReq, err := http.NewRequestWithContext(r.Context(), r.Method, targetURL, bytes.NewReader(body))
if err != nil {
Expand All @@ -567,6 +559,16 @@ func (h *Handler) doUpstreamRequestWithBody(r *http.Request, body []byte) (*http
return h.client.Do(upstreamReq)
}

func (h *Handler) upstreamScheme(r *http.Request) string {
if h.cfg.UpstreamScheme != "" {
return h.cfg.UpstreamScheme
}
if r.TLS == nil {
return "http"
}
return "https"
}

// serveUpstreamResponse forwards an upstream response to the client unchanged.
func (h *Handler) serveUpstreamResponse(w http.ResponseWriter, resp *http.Response) {
defer resp.Body.Close()
Expand Down
Loading