Skip to content

Commit

Permalink
feat(tunnel): add writer parameter
Browse files Browse the repository at this point in the history
Signed-off-by: Dwi Siswanto <[email protected]>
  • Loading branch information
dwisiswant0 committed Oct 25, 2023
1 parent d2393a1 commit 4cd72ff
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 15 deletions.
53 changes: 49 additions & 4 deletions pkg/tunnel/tunnel.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
/*
Package tunnel provides functionality for creating HTTP tunnels
and reverse proxies with [teler] WAF capabilities.
The main components of this package include the [Tunnel] type,
which represents a tunneling configuration, and related functions
and methods for tunnel setup and HTTP request handling.
To create a new tunnel, use the [NewTunnel] function, specifying
the local port, destination address, and optional configuration
parameters. The [Tunnel] type also provides the [Tunnel.ServeHTTP] method
for handling incoming HTTP requests and proxying them to the
destination, analyzing the incoming HTTP request from threats using
the [teler.Teler] middleware.
Additional configuration options can be loaded from YAML or JSON
files, allowing for customizing the [teler] WAF behavior.
*/
package tunnel

import (
"io"
"strings"

"net/http"
Expand All @@ -19,11 +38,26 @@ type Tunnel struct {
Options teler.Options
}

func NewTunnel(port int, dest, cfgPath, optFormat string) (*Tunnel, error) {
// NewTunnel creates a new [Tunnel] instance for proxying HTTP traffic.
//
// Parameters:
// - `port`: The local port on which the tunnel will listen for incoming requests.
// - `dest`: The destination address to which incoming requests will be forwarded.
// - `cfgPath`: The path to a configuration file for additional tunnel options.
// - `optFormat`: The format of the configuration file ("yaml" or "json").
// - `writer`: An optional [io.Writer] where tunnel log output will be written.
// Pass nil to use default [teler] logging only.
//
// Please be aware that when you pass a custom `writer`, the [teler.Options.NoStderr]
// option value will be forcibly set to `true`, regardless of the `no_stderr` value
// that might be loaded from additional configuration options.
func NewTunnel(port int, dest, cfgPath, optFormat string, writer io.Writer) (*Tunnel, error) {
if dest == "" {
return nil, common.ErrDestAddressEmpty
}

// NOTE(dwisiswant0): should we accept the input `dest` parameter
// as pointer of url.URL directly instead of string?
destURL, err := url.Parse(dest)
if err != nil {
return nil, err
Expand Down Expand Up @@ -51,14 +85,25 @@ func NewTunnel(port int, dest, cfgPath, optFormat string) (*Tunnel, error) {
}

tun.Options = opt
tun.Teler = teler.New(opt)
} else {
tun.Teler = teler.New()
}

if writer != nil {
opt.LogWriter = writer
opt.NoStderr = true
}

tun.Teler = teler.New(opt)

return tun, nil
}

// ServeHTTP is a method of the [Tunnel] type, which allows
// the [Tunnel] to implement the [http.Handler] interface.
//
// This method forwards the incoming HTTP request to the
// [httputil.ReverseProxy.ServeHTTP] method, while also
// analyzing the incoming HTTP request from threats using
// the [teler.Teler] middleware.
func (t *Tunnel) ServeHTTP(w http.ResponseWriter, r *http.Request) {
t.Teler.HandlerFuncWithNext(w, r, t.ReverseProxy.ServeHTTP)
}
28 changes: 17 additions & 11 deletions pkg/tunnel/tunnel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func init() {

func TestNewTunnel(t *testing.T) {
// Test case 1: valid destination and no configuration file
tun, err := NewTunnel(8080, dest, "", "")
tun, err := NewTunnel(8080, dest, "", "", nil)
if err != nil {
t.Fatalf("Expected no error, but got: %v", err)
}
Expand All @@ -47,20 +47,20 @@ func TestNewTunnel(t *testing.T) {
}

// Test case 2: invalid destination (empty)
_, err = NewTunnel(8080, "", "", "")
_, err = NewTunnel(8080, "", "", "", nil)
if err != common.ErrDestAddressEmpty {
t.Fatalf("Expected %v, but got: %v", common.ErrDestAddressEmpty, err)
}

// Test case 3: with config file but empty format
_, err = NewTunnel(8080, dest, filepath.Join(workspaceDir, "teler-waf.conf.example.yaml"), "")
_, err = NewTunnel(8080, dest, filepath.Join(workspaceDir, "teler-waf.conf.example.yaml"), "", nil)
if err != common.ErrCfgFileFormatUnd {
t.Fatalf("Expected %v, but got: %v", common.ErrCfgFileFormatUnd, err)
}

// Test case 4: with config file and YAML format
tun = nil
tun, err = NewTunnel(8080, dest, filepath.Join(workspaceDir, "teler-waf.conf.example.yaml"), "yaml")
tun, err = NewTunnel(8080, dest, filepath.Join(workspaceDir, "teler-waf.conf.example.yaml"), "yaml", nil)
if err != nil {
t.Fatalf("Expected no error, but got: %v", err)
}
Expand All @@ -71,7 +71,7 @@ func TestNewTunnel(t *testing.T) {

// Test case 5: with config file and JSON format
tun = nil
tun, err = NewTunnel(8080, dest, filepath.Join(workspaceDir, "teler-waf.conf.example.json"), "json")
tun, err = NewTunnel(8080, dest, filepath.Join(workspaceDir, "teler-waf.conf.example.json"), "json", nil)
if err != nil {
t.Fatalf("Expected no error, but got: %v", err)
}
Expand All @@ -81,26 +81,32 @@ func TestNewTunnel(t *testing.T) {
}

// Test case 6: with config file and xml format
_, err = NewTunnel(8080, dest, filepath.Join(workspaceDir, "teler-waf.conf.example.json"), "xml")
_, err = NewTunnel(8080, dest, filepath.Join(workspaceDir, "teler-waf.conf.example.json"), "xml", nil)
if err != common.ErrCfgFileFormatInv {
t.Fatalf("Expected %v, but got: %v", common.ErrCfgFileFormatInv, err)
}

// Test case 7: invalid destination
tun = nil
tun, _ = NewTunnel(8080, "http://this is not a valid URL", "", "")
tun, _ = NewTunnel(8080, "http://this is not a valid URL", "", "", nil)
if tun != nil {
t.Fatalf("Expected %v, but got: %v", nil, tun)
}

// Test case 8: with invalid config file
_, err = NewTunnel(8080, dest, "nonexistent", "yaml")
_, err = NewTunnel(8080, dest, "nonexistent", "yaml", nil)
if err == nil {
t.Fatal("Expected error, but got nil")
}

// Test case 9: with invalid config file
_, err = NewTunnel(8080, dest, "nonexistent", "json")
_, err = NewTunnel(8080, dest, "nonexistent", "json", nil)
if err == nil {
t.Fatal("Expected no error, but got nil")
}

// Test case 10: with io.Writer
_, err = NewTunnel(8080, dest, "nonexistent", "json", os.Stderr)
if err == nil {
t.Fatal("Expected no error, but got nil")
}
Expand Down Expand Up @@ -138,7 +144,7 @@ func BenchmarkNewTunnel(b *testing.B) {
b.ReportAllocs()

for i := 0; i < b.N; i++ {
_, err := NewTunnel(8080, dest, filepath.Join(workspaceDir, "teler-waf.conf.example.yaml"), "yaml")
_, err := NewTunnel(8080, dest, filepath.Join(workspaceDir, "teler-waf.conf.example.yaml"), "yaml", nil)
if err != nil {
b.Fatalf("Expected no error, but got: %v", err)
}
Expand All @@ -150,7 +156,7 @@ func BenchmarkNewTunnel(b *testing.B) {
b.ReportAllocs()

for i := 0; i < b.N; i++ {
_, err := NewTunnel(8080, dest, filepath.Join(workspaceDir, "teler-waf.conf.example.json"), "json")
_, err := NewTunnel(8080, dest, filepath.Join(workspaceDir, "teler-waf.conf.example.json"), "json", nil)
if err != nil {
b.Fatalf("Expected no error, but got: %v", err)
}
Expand Down

0 comments on commit 4cd72ff

Please sign in to comment.