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

feat(tunnel): add writer parameter #35

Merged
merged 2 commits into from
Oct 25, 2023
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
3 changes: 2 additions & 1 deletion internal/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ func New(opt *common.Options) error {
}

dest := buildDest(opt.Destination)
writer := &logWriter{Logger: opt.Logger}

tun, err := tunnel.NewTunnel(opt.Port, dest, opt.Config.Path, opt.Config.Format)
tun, err := tunnel.NewTunnel(opt.Port, dest, opt.Config.Path, opt.Config.Format, writer)
if err != nil {
return err
}
Expand Down
84 changes: 84 additions & 0 deletions internal/runner/writer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package runner

import (
"encoding/json"

"github.com/charmbracelet/log"
)

type Writer interface {
Write(p []byte) (n int, err error)
}

type logWriter struct {
*log.Logger
}

type data map[string]any

func (w *logWriter) Write(p []byte) (n int, err error) {
var d data

n = len(p)

err = json.Unmarshal(p, &d)
if err != nil {
return 0, err
}

logger := w.WithPrefix("teler-waf")
w.Logger = logger

w.Logger.With("ts", d["ts"], "msg", d["msg"])
w.write(d)

return
}

func (w *logWriter) write(d data) {
switch level := d["level"].(string); level {
case "debug":
w.writeDebug(d)
case "info":
w.writeInfo(d)
case "warn":
w.writeWarn(d)
case "error":
w.writeError(d)
case "fatal":
w.writeFatal(d)
}
}

func (w *logWriter) writeDebug(d data) {
w.Debug(d["msg"], "ts", d["ts"])
}

func (w *logWriter) writeInfo(d data) {
if opt, ok := d["options"].(data); ok {
w.Info(d["msg"],
"ts", d["ts"],
"options", opt,
)
}
}

func (w *logWriter) writeWarn(d data) {
w.Warn(d["msg"],
"ts", d["ts"],
"id", d["id"],
"threat", d["category"],
"request", d["request"],
)
}

func (w *logWriter) writeError(d data) {
w.Error(d["msg"],
"ts", d["ts"],
"source", d["caller"],
)
}

func (w *logWriter) writeFatal(d data) {
w.writeError(d)
}
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
Loading