diff --git a/compose/proxy.go b/compose/proxy.go new file mode 100644 index 0000000..098aa2d --- /dev/null +++ b/compose/proxy.go @@ -0,0 +1,113 @@ +package compose + +import ( + "fmt" + "net" + + "github.com/shoriwe/fullproxy/v3/proxies" + "github.com/shoriwe/fullproxy/v3/utils/network" +) + +const ( + ProxyForward = "forward" + ProxyHTTP = "http" + ProxySocks5 = "socks5" +) + +type Proxy struct { + Type string `yaml:"type" json:"type"` + Listener Network `yaml:"listener" json:"listener"` + Dialer *Network `yaml:"dialer,omitempty" json:"dialer,omitempty"` + Network *string `yaml:"network,omitempty" json:"network,omitempty"` + Address *string `yaml:"address,omitempty" json:"address,omitempty"` + proxy proxies.Proxy +} + +func (p *Proxy) getDialFunc() (network.DialFunc, error) { + switch p.Dialer { + case nil: + return p.Listener.DialFunc() + default: + return p.Dialer.DialFunc() + } +} + +func (p *Proxy) setupForward() (proxy proxies.Proxy, err error) { + var ( + l net.Listener + dialFunc network.DialFunc + ) + l, err = p.Listener.Listen() + if err == nil { + dialFunc, err = p.getDialFunc() + if err == nil { + network.CloseOnError(&err, l) + proxy = &proxies.Forward{ + Network: *p.Network, + Address: *p.Address, + Listener: l, + Dial: dialFunc, + } + } + } + return proxy, err +} + +func (p *Proxy) setupHTTP() (proxy proxies.Proxy, err error) { + var ( + l net.Listener + dialFunc network.DialFunc + ) + l, err = p.Listener.Listen() + if err == nil { + dialFunc, err = p.getDialFunc() + if err == nil { + network.CloseOnError(&err, l) + proxy = &proxies.HTTP{ + Listener: l, + Dial: dialFunc, + } + } + } + return proxy, err +} + +func (p *Proxy) setupSocks5() (proxy proxies.Proxy, err error) { + var ( + l net.Listener + dialFunc network.DialFunc + ) + l, err = p.Listener.Listen() + if err == nil { + dialFunc, err = p.getDialFunc() + if err == nil { + network.CloseOnError(&err, l) + proxy = &proxies.Socks5{ + Listener: l, + Dial: dialFunc, + } + } + } + return proxy, err +} + +func (p *Proxy) setupProxy() (proxies.Proxy, error) { + switch p.Type { + case ProxyForward: + return p.setupForward() + case ProxyHTTP: + return p.setupHTTP() + case ProxySocks5: + return p.setupSocks5() + default: + return nil, fmt.Errorf("unknown proxy type: %s", p.Type) + } +} + +func (p *Proxy) Serve() (err error) { + p.proxy, err = p.setupProxy() + if err == nil { + err = p.proxy.Serve() + } + return err +} diff --git a/compose/proxy_test.go b/compose/proxy_test.go new file mode 100644 index 0000000..8c9043e --- /dev/null +++ b/compose/proxy_test.go @@ -0,0 +1,318 @@ +package compose + +import ( + "context" + "net" + "net/http" + "net/url" + "testing" + + "github.com/gavv/httpexpect/v2" + httputils "github.com/shoriwe/fullproxy/v3/utils/http" + "github.com/shoriwe/fullproxy/v3/utils/network" + "github.com/stretchr/testify/assert" + "golang.org/x/net/proxy" +) + +func TestProxy_getDialFunc(t *testing.T) { + t.Run("no dialer", func(tt *testing.T) { + p := Proxy{ + Listener: Network{ + Type: NetworkBasic, + Network: new(string), + Address: new(string), + }, + } + *p.Listener.Network = "tcp" + *p.Listener.Address = "localhost:0" + dialFunc, err := p.getDialFunc() + assert.Nil(tt, err) + assert.NotNil(tt, dialFunc) + }) + t.Run("with dialer", func(tt *testing.T) { + p := Proxy{ + Listener: Network{ + Type: NetworkBasic, + Network: new(string), + Address: new(string), + }, + Dialer: &Network{ + Type: NetworkBasic, + Network: new(string), + Address: new(string), + }, + } + *p.Listener.Network = "tcp" + *p.Listener.Address = "localhost:0" + *p.Dialer.Network = "tcp" + *p.Dialer.Network = "localhost:0" + dialFunc, err := p.getDialFunc() + assert.Nil(tt, err) + assert.NotNil(tt, dialFunc) + }) +} + +func TestProxy_setupForward(t *testing.T) { + t.Run("Succeed", func(tt *testing.T) { + service := network.ListenAny() + defer service.Close() + httputils.NewMux(service) + p := Proxy{ + Type: ProxyForward, + Network: new(string), + Address: new(string), + Listener: Network{ + Type: NetworkBasic, + Network: new(string), + Address: new(string), + }, + } + *p.Network = service.Addr().Network() + *p.Address = service.Addr().String() + *p.Listener.Network = "tcp" + *p.Listener.Address = "localhost:0" + px, err := p.setupForward() + assert.Nil(tt, err) + defer px.Close() + go px.Serve() + expect := httpexpect.Default(tt, "http://"+px.Addr().String()) + expect.GET(httputils.EchoRoute).Expect().Status(http.StatusOK).Body().Contains(httputils.EchoMsg) + }) +} + +func TestProxy_setupHTTP(t *testing.T) { + t.Run("Succeed", func(tt *testing.T) { + service := network.ListenAny() + defer service.Close() + httputils.NewMux(service) + p := Proxy{ + Type: ProxyHTTP, + Network: new(string), + Address: new(string), + Listener: Network{ + Type: NetworkBasic, + Network: new(string), + Address: new(string), + }, + } + *p.Network = service.Addr().Network() + *p.Address = service.Addr().String() + *p.Listener.Network = "tcp" + *p.Listener.Address = "localhost:0" + px, err := p.setupHTTP() + assert.Nil(tt, err) + defer px.Close() + go px.Serve() + proxyUrl, _ := url.Parse("http://" + px.Addr().String()) + expect := httpexpect.WithConfig( + httpexpect.Config{ + BaseURL: "http://" + service.Addr().String(), + Reporter: httpexpect.NewAssertReporter(t), + Client: &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyURL(proxyUrl), + }, + }, + }, + ) + expect.GET(httputils.EchoRoute).Expect().Status(http.StatusOK).Body().Contains(httputils.EchoMsg) + }) +} + +func TestProxy_setupSocks5(t *testing.T) { + t.Run("Succeed", func(tt *testing.T) { + service := network.ListenAny() + defer service.Close() + httputils.NewMux(service) + p := Proxy{ + Type: ProxySocks5, + Network: new(string), + Address: new(string), + Listener: Network{ + Type: NetworkBasic, + Network: new(string), + Address: new(string), + }, + } + *p.Network = service.Addr().Network() + *p.Address = service.Addr().String() + *p.Listener.Network = "tcp" + *p.Listener.Address = "localhost:0" + px, err := p.setupSocks5() + assert.Nil(tt, err) + defer px.Close() + go px.Serve() + pxDialer, err := proxy.SOCKS5(px.Addr().Network(), px.Addr().String(), nil, nil) + assert.Nil(tt, err) + expect := httpexpect.WithConfig( + httpexpect.Config{ + BaseURL: "http://" + service.Addr().String(), + Reporter: httpexpect.NewAssertReporter(t), + Client: &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return pxDialer.Dial(network, addr) + }, + }, + }, + }, + ) + expect.GET(httputils.EchoRoute).Expect().Status(http.StatusOK).Body().Contains(httputils.EchoMsg) + }) +} + +func TestProxy_setupProxy(t *testing.T) { + t.Run(ProxyForward, func(tt *testing.T) { + service := network.ListenAny() + defer service.Close() + httputils.NewMux(service) + p := Proxy{ + Type: ProxyForward, + Network: new(string), + Address: new(string), + Listener: Network{ + Type: NetworkBasic, + Network: new(string), + Address: new(string), + }, + } + *p.Network = service.Addr().Network() + *p.Address = service.Addr().String() + *p.Listener.Network = "tcp" + *p.Listener.Address = "localhost:0" + px, err := p.setupProxy() + assert.Nil(tt, err) + defer px.Close() + go px.Serve() + expect := httpexpect.Default(tt, "http://"+px.Addr().String()) + expect.GET(httputils.EchoRoute).Expect().Status(http.StatusOK).Body().Contains(httputils.EchoMsg) + }) + t.Run(ProxyHTTP, func(tt *testing.T) { + service := network.ListenAny() + defer service.Close() + httputils.NewMux(service) + p := Proxy{ + Type: ProxyHTTP, + Network: new(string), + Address: new(string), + Listener: Network{ + Type: NetworkBasic, + Network: new(string), + Address: new(string), + }, + } + *p.Network = service.Addr().Network() + *p.Address = service.Addr().String() + *p.Listener.Network = "tcp" + *p.Listener.Address = "localhost:0" + px, err := p.setupProxy() + assert.Nil(tt, err) + defer px.Close() + go px.Serve() + proxyUrl, _ := url.Parse("http://" + px.Addr().String()) + expect := httpexpect.WithConfig( + httpexpect.Config{ + BaseURL: "http://" + service.Addr().String(), + Reporter: httpexpect.NewAssertReporter(t), + Client: &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyURL(proxyUrl), + }, + }, + }, + ) + expect.GET(httputils.EchoRoute).Expect().Status(http.StatusOK).Body().Contains(httputils.EchoMsg) + }) + t.Run(ProxySocks5, func(tt *testing.T) { + service := network.ListenAny() + defer service.Close() + httputils.NewMux(service) + p := Proxy{ + Type: ProxySocks5, + Network: new(string), + Address: new(string), + Listener: Network{ + Type: NetworkBasic, + Network: new(string), + Address: new(string), + }, + } + *p.Network = service.Addr().Network() + *p.Address = service.Addr().String() + *p.Listener.Network = "tcp" + *p.Listener.Address = "localhost:0" + px, err := p.setupProxy() + assert.Nil(tt, err) + defer px.Close() + go px.Serve() + pxDialer, err := proxy.SOCKS5(px.Addr().Network(), px.Addr().String(), nil, nil) + assert.Nil(tt, err) + expect := httpexpect.WithConfig( + httpexpect.Config{ + BaseURL: "http://" + service.Addr().String(), + Reporter: httpexpect.NewAssertReporter(t), + Client: &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return pxDialer.Dial(network, addr) + }, + }, + }, + }, + ) + expect.GET(httputils.EchoRoute).Expect().Status(http.StatusOK).Body().Contains(httputils.EchoMsg) + }) + t.Run("UNKNOWN", func(tt *testing.T) { + service := network.ListenAny() + defer service.Close() + httputils.NewMux(service) + p := Proxy{ + Type: "UNKNOWN", + Network: new(string), + Address: new(string), + Listener: Network{ + Type: NetworkBasic, + Network: new(string), + Address: new(string), + }, + } + *p.Network = service.Addr().Network() + *p.Address = service.Addr().String() + *p.Listener.Network = "tcp" + *p.Listener.Address = "localhost:0" + _, err := p.setupProxy() + assert.NotNil(tt, err) + }) +} + +func TestProxy_Serve(t *testing.T) { + t.Run("Succeed", func(tt *testing.T) { + service := network.ListenAny() + defer service.Close() + httputils.NewMux(service) + p := Proxy{ + Type: ProxyForward, + Network: new(string), + Address: new(string), + Listener: Network{ + Type: NetworkBasic, + Network: new(string), + Address: new(string), + }, + } + *p.Network = service.Addr().Network() + *p.Address = service.Addr().String() + *p.Listener.Network = "tcp" + *p.Listener.Address = "localhost:0" + checkCh := make(chan struct{}, 1) + go func() { + go p.Serve() + for p.proxy == nil { + } + checkCh <- struct{}{} + }() + <-checkCh + expect := httpexpect.Default(tt, "http://"+p.proxy.Addr().String()) + expect.GET(httputils.EchoRoute).Expect().Status(http.StatusOK).Body().Contains(httputils.EchoMsg) + }) +}