Skip to content
This repository has been archived by the owner on Nov 2, 2023. It is now read-only.

Commit

Permalink
agent/backend: use *_PROXY environment variables proxy settings
Browse files Browse the repository at this point in the history
Take into account `{HTTPS,HTTP,NO}_PROXY` environment variables (and their lowercase alternatives) when `SQREEN_PROXY` is not set. This way, the system's configuration is taken into account when defined.

Users not yet using proxies and not willing to impact their normal http requests can just use `SQREEN_PROXY` to explicitly tell the agent to use a given proxy.

Closes SQR-5316
  • Loading branch information
Julio-Guerra committed Jan 23, 2019
2 parents eca5d3d + 440c677 commit d94a387
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 105 deletions.
2 changes: 1 addition & 1 deletion Makefile
Expand Up @@ -86,7 +86,7 @@ test-race: $(needs-dev-container) $(needs-vendors) $(needs-protobufs)
#-----------------------------------------------------------------------------

$(needs-vendors): go.mod $(global-deps) $(needs-dev-container)
$(call dockerize, go mod vendor)
$(call dockerize, go mod vendor -v)
mkdir -p $(@D) && touch $@

.PHONY: vendor
Expand Down
36 changes: 23 additions & 13 deletions agent/backend/client.go
Expand Up @@ -8,12 +8,11 @@ import (
"net/http/httputil"
"net/url"

"github.com/sqreen/go-agent/agent/config"
"github.com/sqreen/go-agent/agent/plog"

"github.com/gogo/protobuf/jsonpb"
"github.com/gogo/protobuf/proto"
"github.com/sqreen/go-agent/agent/backend/api"
"github.com/sqreen/go-agent/agent/config"
"github.com/sqreen/go-agent/agent/plog"
"golang.org/x/net/http/httpproxy"
)

Expand All @@ -26,21 +25,32 @@ type Client struct {
}

func NewClient(backendURL string) (*Client, error) {
proxyCfg := httpproxy.Config{
HTTPSProxy: config.BackendHTTPAPIProxy(),
}
proxyURL := proxyCfg.ProxyFunc()
proxy := func(req *http.Request) (*url.URL, error) {
return proxyURL(req.URL)
var transport *http.Transport
if proxySettings := config.BackendHTTPAPIProxy(); proxySettings == "" {
// No user settings. The default transport uses standard global proxy
// settings *_PROXY environment variables.
logger.Info("using proxy settings as indicated by the environment variables HTTP_PROXY, HTTPS_PROXY and NO_PROXY (or the lowercase versions)")
transport = (http.DefaultTransport).(*http.Transport)
} else {
// Use the settings.
logger.Info("using configured https proxy ", proxySettings)
proxyCfg := httpproxy.Config{
HTTPSProxy: proxySettings,
}
proxyURL := proxyCfg.ProxyFunc()
proxy := func(req *http.Request) (*url.URL, error) {
return proxyURL(req.URL)
}
// Shallow copy the default transport and overwrite its proxy settings.
transportCopy := *(http.DefaultTransport).(*http.Transport)
transport = &transportCopy
transport.Proxy = proxy
}

transport := *(http.DefaultTransport).(*http.Transport)
transport.Proxy = proxy

client := &Client{
client: &http.Client{
Timeout: config.BackendHTTPAPIRequestTimeout,
Transport: &transport,
Transport: transport,
},
backendURL: backendURL,
pbMarshaler: api.DefaultJSONPBMarshaler,
Expand Down
280 changes: 189 additions & 91 deletions agent/backend/client_test.go
Expand Up @@ -4,119 +4,148 @@ import (
"io/ioutil"
math_rand "math/rand"
"net/http"
"os"
"reflect"
time "time"
"testing"
"time"

"github.com/gogo/protobuf/jsonpb"
"github.com/gogo/protobuf/proto"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/ghttp"

"github.com/sqreen/go-agent/agent/backend"
"github.com/sqreen/go-agent/agent/backend/api"
"github.com/sqreen/go-agent/agent/config"
"github.com/sqreen/go-agent/tools/testlib"
"github.com/stretchr/testify/require"
)

var (
seed = time.Now().UnixNano()
popr = math_rand.New(math_rand.NewSource(seed))
)

var _ = Describe("The backend client", func() {
var (
server *ghttp.Server
client *backend.Client
)
func TestClient(t *testing.T) {
RegisterTestingT(t)
g := NewGomegaWithT(t)

t.Run("AppLogin", func(t *testing.T) {
token := testlib.RandString(2, 50)
appName := testlib.RandString(2, 50)

statusCode := http.StatusOK

endpointCfg := &config.BackendHTTPAPIEndpoint.AppLogin

response := api.NewPopulatedAppLoginResponse(popr, false)
err := JSONPBLoopback(response)
g.Expect(err).ToNot(HaveOccurred())

request := api.NewPopulatedAppLoginRequest(popr, false)
err = JSONPBLoopback(request)
g.Expect(err).ToNot(HaveOccurred())

headers := http.Header{
config.BackendHTTPAPIHeaderToken: []string{token},
config.BackendHTTPAPIHeaderAppName: []string{appName},
}

server := initFakeServer(endpointCfg, request, response, statusCode, headers)
defer server.Close()

client, err := backend.NewClient(server.URL())
g.Expect(err).NotTo(HaveOccurred())

res, err := client.AppLogin(request, token, appName)
g.Expect(err).NotTo(HaveOccurred())
// A request has been received
g.Expect(len(server.ReceivedRequests())).ToNot(Equal(0))
g.Expect(res).Should(Equal(response))
})

JustBeforeEach(func() {
var err error
server = ghttp.NewServer()
client, err = backend.NewClient(server.URL())
Expect(err).NotTo(HaveOccurred())
t.Run("AppBeat", func(t *testing.T) {
session := testlib.RandString(2, 50)

statusCode := http.StatusOK

endpointCfg := &config.BackendHTTPAPIEndpoint.AppBeat

response := api.NewPopulatedAppBeatResponse(popr, false)
err := JSONPBLoopback(response)
g.Expect(err).ToNot(HaveOccurred())

request := api.NewPopulatedAppBeatRequest(popr, false)
err = JSONPBLoopback(request)
g.Expect(err).ToNot(HaveOccurred())

headers := http.Header{
config.BackendHTTPAPIHeaderSession: []string{session},
}

server := initFakeServer(endpointCfg, request, response, statusCode, headers)
defer server.Close()

client, err := backend.NewClient(server.URL())
g.Expect(err).NotTo(HaveOccurred())

res, err := client.AppBeat(request, session)
g.Expect(err).NotTo(HaveOccurred())
// A request has been received
g.Expect(len(server.ReceivedRequests())).ToNot(Equal(0))
g.Expect(res).Should(Equal(response))
})

JustAfterEach(func() {
server.Close()
t.Run("Batch", func(t *testing.T) {
t.Skip("need json unmarshaler")
session := testlib.RandString(2, 50)

statusCode := http.StatusOK

endpointCfg := &config.BackendHTTPAPIEndpoint.Batch

request := api.NewPopulatedBatchRequest(popr, false)
err := JSONPBLoopback(request)
g.Expect(err).ToNot(HaveOccurred())

headers := http.Header{
config.BackendHTTPAPIHeaderSession: []string{session},
}

server := initFakeServer(endpointCfg, request, nil, statusCode, headers)
defer server.Close()

client, err := backend.NewClient(server.URL())
g.Expect(err).NotTo(HaveOccurred())

err = client.Batch(request, session)
g.Expect(err).NotTo(HaveOccurred())
// A request has been received
g.Expect(len(server.ReceivedRequests())).ToNot(Equal(0))
})

Describe("request", func() {
var (
endpointCfg *config.HTTPAPIEndpoint
statusCode = http.StatusOK
response proto.Message
request proto.Message
headers http.Header
)

JustBeforeEach(func() {
server.AppendHandlers(ghttp.CombineHandlers(
ghttp.VerifyRequest(endpointCfg.Method, endpointCfg.URL),
ghttp.VerifyHeader(headers),
VerifyJSONPBRepresenting(request),
RespondWithJSONPB(&statusCode, response),
))
})

Describe("AppLogin", func() {
var (
token string = "my-token"
appName string = testlib.RandString(2, 50)
)

BeforeEach(func() {
endpointCfg = &config.BackendHTTPAPIEndpoint.AppLogin

response = api.NewPopulatedAppLoginResponse(popr, false)
err := JSONPBLoopback(response)
Expect(err).ToNot(HaveOccurred())

request = api.NewPopulatedAppLoginRequest(popr, false)
err = JSONPBLoopback(request)
Expect(err).ToNot(HaveOccurred())

headers = http.Header{
config.BackendHTTPAPIHeaderToken: []string{token},
config.BackendHTTPAPIHeaderAppName: []string{appName},
}
})

It("should perform the API call", func() {
res, err := client.AppLogin(request.(*api.AppLoginRequest), token, appName)
Expect(err).NotTo(HaveOccurred())
Expect(res).Should(Equal(response))
})
})

Describe("AppBeat", func() {
var session string = "my-session"

BeforeEach(func() {
endpointCfg = &config.BackendHTTPAPIEndpoint.AppBeat

response = api.NewPopulatedAppBeatResponse(popr, false)
err := JSONPBLoopback(response)
Expect(err).ToNot(HaveOccurred())

request = api.NewPopulatedAppBeatRequest(popr, false)
err = JSONPBLoopback(request)
Expect(err).ToNot(HaveOccurred())

headers = http.Header{
config.BackendHTTPAPIHeaderSession: []string{session},
}
})

It("should perform the API call", func() {
res, err := client.AppBeat(request.(*api.AppBeatRequest), session)
Expect(err).NotTo(HaveOccurred())
Expect(res).Should(Equal(response))
})
})
t.Run("AppLogout", func(t *testing.T) {
session := testlib.RandString(2, 50)

statusCode := http.StatusOK

endpointCfg := &config.BackendHTTPAPIEndpoint.AppLogout

headers := http.Header{
config.BackendHTTPAPIHeaderSession: []string{session},
}

server := initFakeServer(endpointCfg, nil, nil, statusCode, headers)
defer server.Close()

client, err := backend.NewClient(server.URL())
g.Expect(err).NotTo(HaveOccurred())

err = client.AppLogout(session)
g.Expect(err).NotTo(HaveOccurred())
// A request has been received
g.Expect(len(server.ReceivedRequests())).ToNot(Equal(0))
})
})
}

// JSONPBLoopback passes msg through the JSON-PB marshaler and unmarshaler so
// that msg then has the same data has another protobuf parsed from a JSONPB
Expand All @@ -130,7 +159,28 @@ func JSONPBLoopback(msg proto.Message) error {
return jsonpb.UnmarshalString(msgJSON, msg)
}

func RespondWithJSONPB(statusCode *int, object proto.Message, optionalHeader ...http.Header) http.HandlerFunc {
func initFakeServer(endpointCfg *config.HTTPAPIEndpoint, request, response proto.Message, statusCode int, headers http.Header) *ghttp.Server {
handlers := []http.HandlerFunc{
ghttp.VerifyRequest(endpointCfg.Method, endpointCfg.URL),
ghttp.VerifyHeader(headers),
}

if request != nil {
handlers = append(handlers, VerifyJSONPBRepresenting(request))
}

if response != nil {
handlers = append(handlers, RespondWithJSONPB(statusCode, response))
} else {
handlers = append(handlers, ghttp.RespondWith(statusCode, nil))
}

server := ghttp.NewServer()
server.AppendHandlers(ghttp.CombineHandlers(handlers...))
return server
}

func RespondWithJSONPB(statusCode int, object proto.Message, optionalHeader ...http.Header) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
data, err := api.DefaultJSONPBMarshaler.MarshalToString(object)
Expect(err).ShouldNot(HaveOccurred())
Expand All @@ -144,7 +194,7 @@ func RespondWithJSONPB(statusCode *int, object proto.Message, optionalHeader ...
headers["Content-Type"] = []string{"application/json"}
}
copyHeader(headers, w.Header())
w.WriteHeader(*statusCode)
w.WriteHeader(statusCode)
w.Write([]byte(data))
}
}
Expand Down Expand Up @@ -176,3 +226,51 @@ func copyHeader(src http.Header, dst http.Header) {
dst[key] = value
}
}

func TestProxy(t *testing.T) {
// ghttp uses gomega global functions so globally register `t` to gomega.
RegisterTestingT(t)
t.Run("HTTPS_PROXY", func(t *testing.T) { testProxy(t, "HTTPS_PROXY") })
t.Run("SQREEN_PROXY", func(t *testing.T) { testProxy(t, "SQREEN_PROXY") })
}

func testProxy(t *testing.T, envVar string) {
t.Skip()
// FIXME: (i) use an actual proxy, (ii) check requests go through it, (iii)
// use a fake backend and check the requests exactly like previous tests
// (ideally reuse them and add the proxy).
http.DefaultTransport.(*http.Transport).CloseIdleConnections()
// Create a fake proxy checking it receives a CONNECT request.
proxy := ghttp.NewServer()
defer proxy.Close()
proxy.AppendHandlers(ghttp.CombineHandlers(
ghttp.VerifyRequest(http.MethodConnect, ""),
ghttp.RespondWith(http.StatusOK, nil),
))

//back := ghttp.NewUnstartedServer()
//back.HTTPTestServer.Listener.Close()
//listener, _ := net.Listen("tcp", testlib.GetNonLoopbackIP().String()+":0")
//back.HTTPTestServer.Listener = listener
//back.Start()
//defer back.Close()
//back.AppendHandlers(ghttp.CombineHandlers(
// ghttp.VerifyRequest(http.MethodPost, "/sqreen/v1/app-login"),
// ghttp.RespondWith(http.StatusOK, nil),
//))

// Setup the configuration
os.Setenv(envVar, proxy.URL())
defer os.Unsetenv(envVar)
require.Equal(t, os.Getenv(envVar), proxy.URL())

// The new client should take the proxy into account.
client, err := backend.NewClient(config.BackendHTTPAPIBaseURL())
require.Equal(t, err, nil)
// Perform a request that should go through the proxy.
request := api.NewPopulatedAppLoginRequest(popr, false)
_, err = client.AppLogin(request, "my-token", "my-app")
// A request has been received:
//require.NotEqual(t, len(back.ReceivedRequests()), 0, "0 request received")
require.NotEqual(t, len(proxy.ReceivedRequests()), 0, "0 request received")
}

0 comments on commit d94a387

Please sign in to comment.