From 902019a9732fab5de0a9239dcea512b68827c683 Mon Sep 17 00:00:00 2001 From: rcchopra Date: Wed, 20 Aug 2025 21:46:33 +0530 Subject: [PATCH] proxy support --- scm/driver/github/github.go | 28 +++++++++ scm/transport/proxy/proxy.go | 74 +++++++++++++++++++++++ scm/transport/proxy/proxy_test.go | 97 +++++++++++++++++++++++++++++++ 3 files changed, 199 insertions(+) create mode 100644 scm/transport/proxy/proxy.go create mode 100644 scm/transport/proxy/proxy_test.go diff --git a/scm/driver/github/github.go b/scm/driver/github/github.go index 4530e155..7f820523 100644 --- a/scm/driver/github/github.go +++ b/scm/driver/github/github.go @@ -9,15 +9,25 @@ import ( "bytes" "context" "encoding/json" + "net/http" "net/url" "strconv" "strings" "github.com/drone/go-scm/scm" + "github.com/drone/go-scm/scm/transport/proxy" ) // New returns a new GitHub API client. +// This function maintains backward compatibility and creates a client without proxy. func New(uri string) (*scm.Client, error) { + return NewWithProxy(uri, "") +} + +// NewWithProxy returns a new GitHub API client with optional proxy support. +// If proxyURL is empty or nil, no proxy will be used. +// If proxyURL is provided, all HTTP requests will be routed through the specified proxy. +func NewWithProxy(uri, proxyURL string) (*scm.Client, error) { base, err := url.Parse(uri) if err != nil { return nil, err @@ -47,6 +57,15 @@ func New(uri string) (*scm.Client, error) { client.Reviews = &reviewService{client} client.Users = &userService{client} client.Webhooks = &webhookService{client} + + if proxyURL != "" { + transport, err := proxy.NewTransport(http.DefaultTransport, proxyURL) + if err != nil { + return nil, err + } + client.Client.Client = &http.Client{Transport: transport} + } + return client.Client, nil } @@ -57,6 +76,15 @@ func NewDefault() *scm.Client { return client } +// NewDefaultWithProxy returns a new GitHub API client using the +// default api.github.com address with optional proxy support. +// If proxyURL is empty or nil, no proxy will be used. +// If proxyURL is provided, all HTTP requests will be routed through the specified proxy. +func NewDefaultWithProxy(proxyURL string) *scm.Client { + client, _ := NewWithProxy("https://api.github.com", proxyURL) + return client +} + // wraper wraps the Client to provide high level helper functions // for making http requests and unmarshaling the response. type wrapper struct { diff --git a/scm/transport/proxy/proxy.go b/scm/transport/proxy/proxy.go new file mode 100644 index 00000000..92a546bc --- /dev/null +++ b/scm/transport/proxy/proxy.go @@ -0,0 +1,74 @@ +// Copyright 2018 Drone.IO Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package proxy + +import ( + "net/http" + "net/url" +) + +// Transport is an http.RoundTripper that makes HTTP +// requests through a proxy, wrapping a base RoundTripper +type Transport struct { + Base http.RoundTripper + ProxyURL *url.URL +} + +// RoundTrip makes the request through the configured proxy. +func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) { + // If no proxy is configured, use the base transport + if t.ProxyURL == nil { + return t.base().RoundTrip(r) + } + + // Create a new transport with the proxy configuration + proxyTransport := &http.Transport{ + Proxy: func(_ *http.Request) (*url.URL, error) { + return t.ProxyURL, nil + }, + } + + // If we have a base transport, copy its configuration + if t.Base != nil { + if baseTransport, ok := t.Base.(*http.Transport); ok { + proxyTransport.TLSClientConfig = baseTransport.TLSClientConfig + proxyTransport.DialContext = baseTransport.DialContext + proxyTransport.MaxIdleConns = baseTransport.MaxIdleConns + proxyTransport.MaxIdleConnsPerHost = baseTransport.MaxIdleConnsPerHost + proxyTransport.IdleConnTimeout = baseTransport.IdleConnTimeout + proxyTransport.TLSHandshakeTimeout = baseTransport.TLSHandshakeTimeout + proxyTransport.ExpectContinueTimeout = baseTransport.ExpectContinueTimeout + } + } + + return proxyTransport.RoundTrip(r) +} + +// base returns the base transport. If no base transport +// is configured, the default transport is returned. +func (t *Transport) base() http.RoundTripper { + if t.Base != nil { + return t.Base + } + return http.DefaultTransport +} + +// NewTransport creates a new proxy transport with the given proxy URL. +// If proxyURL is empty or nil, it returns the base transport unchanged. +func NewTransport(base http.RoundTripper, proxyURL string) (http.RoundTripper, error) { + if proxyURL == "" { + return base, nil + } + + parsedURL, err := url.Parse(proxyURL) + if err != nil { + return nil, err + } + + return &Transport{ + Base: base, + ProxyURL: parsedURL, + }, nil +} diff --git a/scm/transport/proxy/proxy_test.go b/scm/transport/proxy/proxy_test.go new file mode 100644 index 00000000..b0a602ae --- /dev/null +++ b/scm/transport/proxy/proxy_test.go @@ -0,0 +1,97 @@ +// Copyright 2018 Drone.IO Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package proxy + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" +) + +func TestNewTransport_EmptyProxyURL(t *testing.T) { + base := http.DefaultTransport + transport, err := NewTransport(base, "") + if err != nil { + t.Fatalf("Expected no error for empty proxy URL, got: %v", err) + } + if transport != base { + t.Error("Expected transport to be the same as base for empty proxy URL") + } +} + +func TestNewTransport_InvalidProxyURL(t *testing.T) { + base := http.DefaultTransport + _, err := NewTransport(base, "://invalid") + if err == nil { + t.Error("Expected error for invalid proxy URL") + } +} + +func TestNewTransport_ValidProxyURL(t *testing.T) { + base := http.DefaultTransport + proxyURL := "http://proxy.example.com:8080" + transport, err := NewTransport(base, proxyURL) + if err != nil { + t.Fatalf("Expected no error for valid proxy URL, got: %v", err) + } + + proxyTransport, ok := transport.(*Transport) + if !ok { + t.Fatal("Expected transport to be of type *Transport") + } + + expectedURL, _ := url.Parse(proxyURL) + if proxyTransport.ProxyURL.String() != expectedURL.String() { + t.Errorf("Expected proxy URL %s, got %s", expectedURL.String(), proxyTransport.ProxyURL.String()) + } +} + +func TestTransport_RoundTrip_NoProxy(t *testing.T) { + // Create a test server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + })) + defer server.Close() + + transport := &Transport{ + Base: http.DefaultTransport, + ProxyURL: nil, + } + + client := &http.Client{Transport: transport} + resp, err := client.Get(server.URL) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Errorf("Expected status OK, got: %d", resp.StatusCode) + } +} + +func TestTransport_RoundTrip_WithProxy(t *testing.T) { + // Create a test server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + })) + defer server.Close() + + proxyURL, _ := url.Parse("http://proxy.example.com:8080") + transport := &Transport{ + Base: http.DefaultTransport, + ProxyURL: proxyURL, + } + + // This test verifies that the transport is configured with the proxy + // The actual proxy behavior would require a real proxy server for testing + // We're just ensuring the transport is properly configured + if transport.ProxyURL.String() != proxyURL.String() { + t.Errorf("Expected proxy URL %s, got %s", proxyURL.String(), transport.ProxyURL.String()) + } +}