Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add proxy server feature. #41

Closed
Closed
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* Remove backward compatibility with previous versions to go 1.13
* Add `-watcher` flag to reload the server with any changes on `imposters` folder
* Fix searching imposter files mechanism
* Add proxy server feature

## v0.3.3 (2019/05/11)

Expand Down
6 changes: 6 additions & 0 deletions cmd/killgrave/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,16 @@ func runServer(host string, port int, cfg killgrave.Config) server.Server {
Handler: handlers.CORS(server.PrepareAccessControl(cfg.CORS)...)(router),
}

proxyServer, err := server.NewProxy(cfg.Proxy.Url, cfg.Proxy.Mode)
if err != nil {
log.Fatal(err)
}

s := server.NewServer(
cfg.ImpostersPath,
router,
httpServer,
proxyServer,
)
if err := s.Build(); err != nil {
log.Fatal(err)
Expand Down
47 changes: 43 additions & 4 deletions internal/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package killgrave

import (
"errors"
"fmt"
"io/ioutil"
"os"
Expand All @@ -11,10 +12,11 @@ import (

// Config representation of config file yaml
type Config struct {
ImpostersPath string `yaml:"imposters_path"`
Port int `yaml:"port"`
Host string `yaml:"host"`
CORS ConfigCORS `yaml:"cors"`
ImpostersPath string `yaml:"imposters_path"`
Port int `yaml:"port"`
Host string `yaml:"host"`
CORS ConfigCORS `yaml:"cors"`
Proxy ConfigProxy `yaml:"proxy"`
}

// ConfigCORS representation of section CORS of the yaml
Expand All @@ -26,6 +28,43 @@ type ConfigCORS struct {
AllowCredentials bool `yaml:"allow_credentials"`
}

// ConfigProxy is a representation of section proxy of the yaml
type ConfigProxy struct {
Url string `yaml:"url"`
Mode ProxyMode `yaml:"mode"`
}

// ProxyMode is enumeration of proxy server modes
type ProxyMode int
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
type ProxyMode int
type ProxyMode uint8

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need specifically uint8?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we prefer declare the enums with a more accurate type possible as standard, 8bits is more than enough to a enum and in most case u don't need negative numbers, so uint8 is a elegant solution


const (
// ProxyNone server is off
ProxyNone ProxyMode = iota
// ProxyMissing handle only missing requests are proxied
ProxyMissing
// ProxyAll all requests are proxied
ProxyAll
)

// UnmarshalYAML implementation of yaml.Unmarshaler interface
func (mode *ProxyMode) UnmarshalYAML(unmarshal func(interface{}) error) error {
aperezg marked this conversation as resolved.
Show resolved Hide resolved
var textMode string
if err := unmarshal(&textMode); err != nil {
return err
}
switch textMode {
case "all":
*mode = ProxyAll
case "missing":
*mode = ProxyMissing
case "none":
*mode = ProxyNone
default:
return errors.New("unknown proxy mode: " + textMode)
}
return nil
}

// ConfigOpt function to encapsulate optional parameters
type ConfigOpt func(cfg *Config) error

Expand Down
39 changes: 39 additions & 0 deletions internal/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,45 @@ func TestNewConfig(t *testing.T) {
}
}

func TestProxyModeUnmarshal(t *testing.T) {
testCases := map[string]struct {
input interface{}
expected ProxyMode
err error
}{
"valid mode all": {"all", ProxyAll, nil},
"valid mode missing": {"missing", ProxyMissing, nil},
"valid mode none": {"none", ProxyNone, nil},
"empty mode": {"", ProxyNone, errors.New("error")},
"invalid mode": {"nonsens23e", ProxyNone, errors.New("error")},
"error input": {123, ProxyNone, errors.New("error")},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
var mode ProxyMode
err := mode.UnmarshalYAML(func(i interface{}) error {
s := i.(*string)
input, ok := tc.input.(string)
if !ok {
return errors.New("error")
}
*s = input
return nil
})
if err != nil && tc.err == nil {
t.Fatalf("not expected any erros and got %v", err)
}

if err == nil && tc.err != nil {
t.Fatalf("expected an error and got nil")
}
if tc.expected != mode {
t.Fatalf("expected: %v, got: %v", tc.expected, mode)
}
})
}
}

func validConfig() Config {
return Config{
ImpostersPath: "test/testdata/imposters",
Expand Down
32 changes: 32 additions & 0 deletions internal/server/http/proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package http

import (
"net/http"
"net/http/httputil"
"net/url"

killgrave "github.com/friendsofgo/killgrave/internal"
)

// Proxy represent reverse proxy server.
type Proxy struct {
server *httputil.ReverseProxy
mode killgrave.ProxyMode
}

// NewProxy creates new proxy server.
func NewProxy(rawurl string, mode killgrave.ProxyMode) (*Proxy, error) {
u, err := url.Parse(rawurl)
if err != nil {
return nil, err
}
reverseProxy := httputil.NewSingleHostReverseProxy(u)
return &Proxy{reverseProxy, mode}, nil
}

// Handler returns handler that sends request to another server.
func (p *Proxy) Handler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
p.server.ServeHTTP(w, r)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you need to update the headers to allow for SSL redirection:

                r.URL.Host = p.url.Host
		r.URL.Scheme = p.url.Scheme
		r.Header.Set("X-Forwarded-Host", r.Header.Get("Host"))
		r.Host = p.url.Host

For that you will need to persist the result of url.Parse into the proxy struct.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested with the comment that I suggested and, all working very fine :)

}
}
66 changes: 66 additions & 0 deletions internal/server/http/proxy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package http

import (
"errors"
"net/http"
"net/http/httptest"
"testing"

killgrave "github.com/friendsofgo/killgrave/internal"
)

func TestNewProxy(t *testing.T) {
testCases := map[string]struct {
rawUrl string
mode killgrave.ProxyMode
err error
}{
"valid all": {"all", killgrave.ProxyAll, nil},
"valid mode none": {"none", killgrave.ProxyNone, nil},
"error rawUrl": {":http!/gogle.com", killgrave.ProxyNone, errors.New("error")},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
proxy, err := NewProxy(tc.rawUrl, tc.mode)
if err != nil && tc.err == nil {
t.Fatalf("not expected any erros and got %v", err)
}

if err == nil && tc.err != nil {
t.Fatalf("expected an error and got nil")
}
if err != nil {
return
}
if tc.mode != proxy.mode {
t.Fatalf("expected: %v, got: %v", tc.mode, proxy.mode)
}
})
}
}

func TestProxyHandler(t *testing.T) {
isRequestHandled := false
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
isRequestHandled = true
}))
defer backend.Close()

proxy, err := NewProxy(backend.URL, killgrave.ProxyAll)
if err != nil {
t.Fatal("NewProxy failed: ", err)
}

frontend := httptest.NewServer(proxy.Handler())
defer frontend.Close()

_, err = http.Get(frontend.URL)
if err != nil {
t.Fatal("Frontend GET method failed: ", err)
}
if isRequestHandled != true {
t.Fatal("Request was not proxied to backend server")
}

}
18 changes: 16 additions & 2 deletions internal/server/http/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,16 @@ type Server struct {
impostersPath string
router *mux.Router
httpServer http.Server
proxy *Proxy
}

// NewServer initialize the mock server
func NewServer(p string, r *mux.Router, httpServer http.Server) Server {
func NewServer(p string, r *mux.Router, httpServer http.Server, proxyServer *Proxy) Server {
return Server{
impostersPath: p,
router: r,
httpServer: httpServer,
proxy: proxyServer,
}
}

Expand Down Expand Up @@ -73,6 +75,9 @@ func PrepareAccessControl(config killgrave.ConfigCORS) (h []handlers.CORSOption)
// Build read all the files on the impostersPath and add different
// handlers for each imposter
func (s *Server) Build() error {
if s.proxy.mode == killgrave.ProxyAll {
s.handleAll(s.proxy.Handler())
}
if _, err := os.Stat(s.impostersPath); os.IsNotExist(err) {
return fmt.Errorf("%w: the directory %s doesn't exists", err, s.impostersPath)
}
Expand All @@ -83,6 +88,7 @@ func (s *Server) Build() error {
findImposters(s.impostersPath, imposterFileCh)
done <- true
}()
loop:
for {
select {
case f := <-imposterFileCh:
Expand All @@ -97,9 +103,13 @@ func (s *Server) Build() error {
case <-done:
close(imposterFileCh)
close(done)
return nil
break loop
}
}
if s.proxy.mode == killgrave.ProxyMissing {
s.handleAll(s.proxy.Handler())
}
return nil
}

// Run run launch a previous configured http server if any error happens while the starting process
Expand Down Expand Up @@ -155,3 +165,7 @@ func (s Server) unmarshalImposters(imposterFileName string, imposters *[]Imposte
}
return nil
}

func (s *Server) handleAll(h http.HandlerFunc) {
s.router.PathPrefix("/").HandlerFunc(h)
}
Loading