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
28 changes: 28 additions & 0 deletions scm/driver/github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand All @@ -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 {
Expand Down
74 changes: 74 additions & 0 deletions scm/transport/proxy/proxy.go
Original file line number Diff line number Diff line change
@@ -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
}
97 changes: 97 additions & 0 deletions scm/transport/proxy/proxy_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
}